Golang基础--字符串编码(Unicode&UTF-8&Rune)
本文主要是介绍Golang字符的编码以及存储。主要涉及以下几方面:1.几种字符串编码形式(ASCII, UNICODE, UTF-8)之间的关系;2.golang中的rune。
开头
首先要理解一个问题,无论是什么文字或者符号都是计算机不同的展示方式,在内部存储的都是一个个字节,所有语言都是一样的,因为计算机只能认识0和1。所以你要存储所谓的字符,计算机也是将其翻译成0和1来存储。对计算机来说,没有所谓的字符,都是一些字节而已。那这些字符是如何按照0,1存储,最终有如何解释的呢?这个不同的语言是不同的。这篇文章只说说golang里面的表示。
从一个例子说起
1func main() {
2 str1 := "enlish"
3 fmt.Printf("plain string: %s \n",str1)
4 fmt.Printf("hex bytes: ")
5 for i := 0; i < len(str1); i++ {
6 fmt.Printf("%x ", str1[i])
7 }
8 fmt.Printf("\n")
9 for i := 0; i < len(str1); i++ {
10 fmt.Printf("%q ", str1[i])
11 }
12 fmt.Printf("\n")
13
14 str2 := "中国人"
15 fmt.Printf("plain string: %s \n",str2)
16 fmt.Printf("hex bytes: ")
17 for i := 0; i < len(str2); i++ {
18 fmt.Printf("%x ", str2[i])
19 }
20 fmt.Printf("\n")
21 for i := 0; i < len(str2); i++ {
22 fmt.Printf("%q ", str2[i])
23 }
24 fmt.Printf("\n")
25}
输出结果:
1plain string: enlish
2hex bytes: 65 6e 6c 69 73 68
3'e' 'n' 'l' 'i' 's' 'h'
4plain string: 中国人
5hex bytes: e4 b8 ad e5 9b bd e4 ba ba
6'ä' '¸' '\u00ad' 'å' '\u009b' '½' 'ä' 'º' 'º'
但是对中文 "中国人",发现不一样了。首先中文明显使用了更多的字节来存储,而且在针对每个单独的字节按照ASCII进行输出的时候,发现输出的并不是我们的一开始设置的一个个中文"字",而是一些不认识的字符。到这里发现golang好像又不是按照ASCII来存储的,因为我们知道ASCII码值最只到127,里面并没有中文字符,那golang里到底是按照什么存储字符的呢?
字符编码-ASCII
我们首先要有个基础的认知,那就是我们在按照0和1存储字符的时候,一定是经过某种形式的"翻译"的,也就是肯定是有种规则来指导字符和0和1序列的相互转换。这里所说的规则就是字符集。
先来看一个字节的二进制表示:01100101,现在问问自己这个表示什么意思,你是不是不认识?因为这是计算机的语言,要让我们人能看懂,我们需要个 "翻译" ,将其译成我们看懂的语言。那我们就人为的规定每个字符在计算机里怎么表示,形成一个对照表。拿英语来说,e在计算机里就表示为145,也就是刚刚的二进制表示01100101,那计算机在输出的时候,只要一查这个对照表就能准确的输出的e这个字符,这样我们就能看懂了。这里的编码(01100101 = e )就是字符编码,而一个个的字符编码就是字符集。而ASCII表就是这样一个字符集,ASCII码一共规定了128个字符的编码,也就是一个字节的7位,最前面的一位永远是0,这对英文使用者来说已经够了,但是对非英语使用者来说,这还不够。
扩展ASCII码表
这里的不够不仅仅是说一个字节256个字符不能表示一门语言,而是说在用来表示不同语言的时候会有混淆。首先由于先发优势,0~127已经在ASCII有明确的定义,而且已经在全世界流行使用了,这时候如果你要表示其他的字符,就只能用剩下的128位来表示。这时候一些国家或者企业就将字节的最高位也拿来编码,比如上面输出的ä,可以认认为属于一种扩展ASCII码表(其实是Unicode码点,下一节会讲到),对应的二进制为11100100。IBM-PC 有一些后来被称为OEM字符集的东西,它为欧洲语言提供了一些重音字符和一堆画线字符……水平条、垂直条、右侧带有小吊坠的水平条等,您可以使用这些画线字符来制作不同的图形。但是不同国家制定的标准是不一样的。比如说字符代码130显示为é,但在以色列的计算机中显示的却是希伯来语字Gimel。如果真的这样发展下去,那大家都没法使用这剩余的128位,最终这种混乱局面被编入了ANSI标准。
在ANSI标准中,每个人都同意在128以下做什么,其实就是按照ASCII来编码。但是有很多不同的方法来处理 128 及以上的字符,具体取决于您居住的地方。不同地区的不同系统会去使用不同的代码页(其实就是不同的字符集)来进行字符的编码和翻译。例如,在以色列DOS使用名为862的代码页,而希腊用户使用737。它们在128以下相同,但与128以上不同,t特殊的字母都驻留在 128 以上。MS-DOS 的国家版本有几十个这样的代码页,可以处理从英语到冰岛语的所有内容。但是,在同一台计算机上显示希伯来语和希腊语是完全不可能的,因为你不可能一个系统处理显示不同的代码页。而且这还没说到亚洲的语言,因为亚洲的语言更复杂,有几千或者几十万个字符表示,一个字节是无法容纳这些所有的字符的,可能需要两个甚至更多的字节表示。这就导致一个什么问题?你从亚洲发一封邮件到欧洲,显示出来的很可能就是一大堆的乱码表示。
到这里可以发现,目前的字符编码主要有两个缺陷,一个就是不同的地区使用的是不同的字符集,无法统一。第二个就是无法将全世界所有语言进行字符编码.这个时候,Unicode出现了。
Unicode
Unicode所做的就是统一所有语言的字符的编码表示,用单一字符集表示地球上所有的字符,每一个符号都给予一个独一无二的编码(在Unicode里称为代码点),那么乱码问题就会消失。
Unicode是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639表示阿拉伯字母Ain,U+0041表示英语的大写字母A,U+4E25表示汉字严(上面示例输出的\u00ad和\u009b就是unicode码)。具体的符号对应表,可以查看unicode官网,或者专门的汉字对应表。U+XXXX这种编码形式在unicode里叫做code point(代码点),也是一种字符编码,所以Unicode也就是一种字符集,跟ASCII所起到作用是一样的,但是Unicode的存储可没有ASCII那么简单。
比如计算机里存储了两个字节:4E25,请问这两个字节到底是按照Unicode翻译成 '严' , 还是按照ASCII翻译成 N% 呢?这是遇到的第一个问题,也就是如何才能区别 Unicode 和 ASCII?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?
再来看一个,Hello 转成unicode码表示为: U+0048 U+0065 U+006C U+006C U+006F
这里显示是一堆代码点,那如何存储这些代码点呢?unicode早期的想法是将这些统一都存在两个字节中(所以有人认为unicode都是两个字节的),就变成了:00 48 00 65 00 6C 00 6C 00 6F
但真的太浪费空间了。这就是Unicode代码点编码所遇到的第二个问题,如何合理的表示以及存储这些代码点? 为了解决这些问题(这里还有大小端的顺序问题,为了不影响理解的顺序性,这里不做介绍,有兴趣的同学可以自行网上查找相关知识),就出现了多种不同的编码形式,比如UCS-2(因为它有两个字节)或 UTF-16(因为它有 16 位),UTF-8等。这里UTF-8用的最多,所以本篇文章主要来说说UTF-8。
UTF-8
首先声明一点,UTF-8只是表示Unicode一种表示形式,你也可以用比如上面说的UCS-2或者UTF-x编码形式来表示unicode的代码点,你甚至可以使用老旧的OEM编码,只要能将Unicode编码正确的存储以及翻译出来就行。但是你用这些编码存储的unicode代码点,其他计算机不一定能认识,主要是这些编码形式不怎么通用,比如你用UCS-2编码了一个Unicdode代码点,另一台计算机用的UTF-8那就没法认识这个字节表示。所以一般建议直接使用UTF-8编码,这是目前最流行的编码形式,现在就来说说这个UTF-8编码。 UTF-8最大的特点就是它是变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。UTF-8 的编码规则很简单,只有二条:
- 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
- 对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
可以发现在UTF-8 中,从0到127的每个代码点都存储在单个字节中。只有代码点128及以上才使用2、3个字节(实际上最多6个字节)来存储。
下面,还是以汉字严为例,演示如何实现 UTF-8 编码。
严的 Unicode 是 4E25(100111000100101),根据上表,可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),
因此严的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,严的 UTF-8 编码是
11100100 10111000 10100101,转换成十六进制就是E4B8A5.
到这里,字符编码的相关知识就已经说完了。为什么用这么多篇幅讲这些编码知识,因为golang就是以UTF-8进行编码的。
golang中所有字符都是UTF-8编码的,不仅如此,GO的源代码都是被定义为UTF-8,也就是说在golang中任何字符串都按照UTF-8形式存储。回到我们一开始举得例子:65 其实是UTF-8对Unicode代码点U+0065的表示(其实等同于ASCII码,因为UTF-8针对<128的字符与ASCII是一致的)。而 e4 就是Unicode代码点 U+00e4,表示的就是 ä ,ad 由于是不可打印字符,所以直接显示了Unicode代码点 '\u00ad' ,到这里我们已经能够知道,例子中输出的字符都是什么含义了。
到这里,我们已经算是彻底知道golang里如何进行字符串编码和表示的了,那还有最后一个问题,为什么"中国人"输出来的是一堆代码点,而不是输出来的对应的字符呢?以及如何能够分字符输出'中','国','人'。
golang中的代码点--rune
为什么输出来的是一堆代码点?文章一开始的时候我们说了,字符在计算机中都是一个个字节表示,当我们用for 下标去遍历的时候,只是遍历的一个个字节,再将字节按照字符编码进行转义,golang中就是按照utf-8转义。比如e4,按照我们上面所说的UTF8和Unicode转换算法,我们知道e4就是代码点u+00e4,而对应的可输出文字'ä'。
那怎样才能正确输出'中','国','人'三个字呢?我们只要正确取到对应的代码点即可。'中' 的unicode为U+4e2d,对应的UTF-8为 0xE4 0xB8 0xAD,这与我们样例输出是一致的,这也验证了我们前面说的golang的字符串值是UTF-8编码。我们再试着将这个UTF-8编码值输出:
1func main() {
2 hexStr := "e4b8ad"
3 data, _ := hex.DecodeString(hexStr)
4 fmt.Printf("%s\n",data)
5}
那如何可以输出这些中文字符呢?换个问法,就是golang里面如何正确去表示非英文字符呢?或者准确的说如何去表示这个UTF-8编码的代码点呢?用代码点实在有点拗口,所以golang引入了rune类型。该类型完全等同于代码点,它其实是int32的别名,所以我们知道它始终占据四个字节(utf-8是变长的)。现在我们可以尝试使用rune输出我们的'中'.
1func main() {
2 var r rune = '\u4e2d'
3 fmt.Printf("%q\n",r)
4}
1'\141' // 141是97的八进制表示
2'\x61' // 61是97的十六进制表示
3'\u0061'
4'\U00000061'
总而言之,只需要记住三点:
- Rune在golang中代表的是一个UTF-8编码的Unicode代码点;
- 由于是int32类型,所以其实可以存储任何整型值;
- Rune式固定的4字节,而UTF-8是变长的.
By the way,上面的样例我们已经可以看到常规的for循环只是输出了原始的字节,并不能输出我们单个中文字符。那我们可以借助golang中另一种形式的遍历进行rune形式表示的代码点输出.
输出:
1func main() { 2 const chinese = "中国人" 3 for index, runeValue := range chinese { 4 fmt.Printf("%#U 从字节位置 %d 开始\n", runeValue, index) 5 } 6}
详细的使用可以参考golang的UTF8库.1U+4E2D '中' 从字节位置 0 开始 2U+56FD '国' 从字节位置 3 开始 3U+4EBA '人' 从字节位置 6 开始
总结
到这里,我们基本聊完了golang中的字符/字符串存储,编码和显示。我们做下总结:
- golang的字符串只是一个只读的字节切片,它并不是一个个字符组成,这个是跟其他语言不同的地方。
- golang的字符基本都是以UTF-8存储的,如果字符串中含义转义字符,那就可能不是UTF-8编码存储的(可能不是正确的utf-8编码)。
- UTF-8只是Unicode一种编码存储形式,用以存储和翻译一个Unicode代码点.除了UTF-8外,还有其他很多编码形式表示Unicode代码点.
- golang中对代码点有一个特殊的表示形式,就是Rune,它是int32的别名,固定为4字节.
- golang中没有字符的含义,只有原始字节.