Go 语言的string以及utf8编码

Go 语言的string以及utf8编码

Go对于字符串提供了,string 类型。同时还有rune和byte类型可以用于字符串的转换。

先说说 string 类型的一些特性以及这些特性导致的问题。

字符串不可更改:一个字符串一旦被创建,就不能被更改了。
1、如果需要对该字符串做写操作,比如 a + b 本质是创建了一个新的字符串,对于频繁写入一个字符串,尤其是大字符串性能损耗巨大,因为本质是在不停的copy新的内存。
2、基于 string 类型虽然提供了索引查找操作,例如 str[1] 取出下标为1的字节,但无法做赋值操作。
3、由于字符串不可更改的特性,所以两个字符串共享底层变量也是安全的,赋值仍和长度字符串都非常廉价,切片操作也是安全且廉价。

Question:在实际测试的时候,发现赋值的新字符串和旧字符串的内存指向不同。可能是由于字符串是由一个结构体构成的,该结构体被重新复制了一份,所以内存地址不同。还有一个问题是共享底层时,发生了并发写入该如何解决数据冲突呢?底层是如何解决数据临界区问题的呢?

处理字符串时,需要注意一下字节和字符的问题,使用数组下标读取 例如 arr[1] 是读取的字节,而在使用 rang关键字时,则是按照字符来遍历的。Go语言对于UTF8字符的助理非常好,这可能是因为Go的三巨头也是UTF8的发明人的原因吧。

UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示总共有多少编码个字节。如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,ASCII字符每个字符依然是一个字节,和传统的ASCII编码兼容。如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。更大的Unicode码点也是采用类似的策略处理。取值范围如下所示

0xxxxxxx                             runes 0-127    (ASCII)
110xxxxx 10xxxxxx                    128-2047       (values <128 unused)
1110xxxx 10xxxxxx 10xxxxxx           2048-65535     (values <2048 unused)
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536-0x10ffff (other values unused)

总结:
1、0开头的表示单字节,可用 7个bit
2、1开头的,用1的数量表示字节个数,假设表示x字节(1<x<=4),则 5x+1个字节可用,多字节的编码效率在 0.75 – 0.65之间,字节越多损失越大。

Go语言提供了直接编写Unicode的能力,比如下面这样可以直接输出中文 “世”

fmt.Println(“\U00004e16”,string(0x4e16))

但是有一个问题是,如果这样写就无法输出对应的汉字,这是为什么呢?

fmt.Println(string([]byte{0x4e,0x16}))

在上面的字符码表中,我们已经看到了UTF8编码对于表示信息来说是并不紧凑的,因为含有了UTF8的标示位,多字节字符只有约70%的的bit可以用于表示数据。所以,一个汉字的Unicode和Utf8表示是不同的。还是以汉字“世”为例,他的Unicode是 0x4e16,我们展开0x4e16的二进制是 0100111000010110,这是该汉字在Unicode码表中的位置,但并不是一个UTF8的字符,转换成UTF8字符就是 0xe4b896,展开0xe4b896的二进制是 111001001011100010010110 。我们发现把 0xe4b896 的符号位去除后,就是0x4e16。

01001110 00010110
11100100 10111000 10010110 -> 0100 111000 010110 -> 01001110 0010110

那么Go语言在哪些情况下会认为你传入的Unicode码值呢?哪些情况下会认为你传入的是UTF8编码值呢?

以“世”字为例,该汉字的Unicode码是 0x4e16,UTF8编码是0xe4b896,下面这行代码都可以正常的输出“世”。也就是说,\U字面量表示,string 类型强转入参数字,都视为Unicode码的输入。而字节数组转string则将数据视为UTF8编码,也就是已经编码过的数据。

fmt.Println(“\U00004e16”, string(19990), string(rune(19990)), string([]byte{0xe4, 0xb8, 0x96}))

还有一个问题是rune类型有什么用?从实现来看rune只不过是int32的一个别名,Go认为一个rune可以表示一个unicode码点,在遍历字符的时候,返回值可以用一个rune表示。但是如果要用直接rune直接表示字符串的话,我认为有一些浪费,失去了utf8变长编码节约空间的意义。

总结Unicode码和UTF8编码的关系。

Unicode是一个字符集码表,在这个表上约定了数字到字符的映射关系,而UTF8编码则是一个编码规则,定义的是当这些数字要连续排列时,怎样做到节约空间并且消除歧义。

节约空间和消除歧义:因为互联网上大多数常用字符都只需要ASCII码表示(全球来看大多是英文),为所有字符分配4字节的存储空间是不合适的,所以就需要变长表示,但是变长将会引起上下文歧义,一个4字节的字符可能会被理解为4个单字节字符,所以编码规则实现了消除歧义以及变长统计的方法。

这么来看说UTF8编码是一种数据压缩算法也不为过,它的假设前提是互联网的大部分内容都是ASCII码,把长编码给出现频次低的字符,短编码给出现频次高的字符。这和压缩算法的哈夫曼编码不是异曲同工之妙吗。

发表评论

邮箱地址不会被公开。 必填项已用*标注