UTF-8 编码在 Golang 中的解读

Go 语言的源文件总是按 UTF-8 编码,并且习惯上 Go 语言的字符串会按 UTF-8 解读,所以在源码中我们可以将 Unicode 码点写入字符串字面量。这篇文章将让你了解,在 Go 语言的编码中,字母、数字、标点符号、中文等等是如何被编码。
 

ASCII


ASCII —— 美国信息交换标准码,它使用 7 位表示 127 个字符,大小写英文字母、数字、各种标点和设备控制符。这对早期的计算机行业已经足够了,但是让世界上众多使用其他语言的人无法在计算机上使用自己的文书体系。

比如

fmt.Printf("二进制: %b\n\n", 'a')

输出:
二进制: 1100001

fmt.Println(string(20154)) // 数字
fmt.Println(string(0x4eba))  // 十六进制
上面两个都输出: "人"
 

Unicode


到底怎样才能应付语言的繁杂多样,还能兼顾效率呢?Unicode,它包括了世界上所有文书体系的全部字符,目前最新版本 Unicode 12.1,(https://www.unicode.org/versions/Unicode12.1.0/),它现已包含 137,929 字符,网上有些说是100多万是错了,读者可以自己看官方文档。不然可以看我下面截图,见 Summary 标题下一行。
那问题来了,字符被如何标示呢,答:每个字符唯一对应一个 int32 类型的数字,比如“人”对应的数字“20154”,go 语言中,有个 rune 类型,它是 int32 的类型的别名,它的值能代表一个字符。还有一个字节类型 byte,它的值代表一个字节。
 

UTF-8


UTF-8 以字节为单位对 Unicode 码点作变长编码。UTF-8 是现行的一种 Unicode 标准,由 Go 语言的两位创建者 Ken Thompson 和 Rob Pike 发明。每个文字符合用 1~4 个字节表示(与前面说的每个字符对应一个数字并不矛盾,它们可以相互转换,后面将会提供转换的规制),ASCII 字符的编码仅占 1 一个字节,而其他常用的文书字符的编码只有 2 或 3 个字节。一个文字符号编码的首字母的高位指明了后面还有几个字节。
0xxxxxxx,8个位的0/1,最高位是 0,表明它是 7 位的ASCII 码
110xxxxx 10xxxxxx,最高几位是 110,它是两个字节的字符,其中第二个字节以 10 开头
1110xxxx 10xxxxxx 10xxxxxx,最高位是 1110,它是三个字节的字符,其中第二、第三字节以 10 开头
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx,最高位是 11110,它是四个字节的字符,其中第二、第三、第四字节以 10 开头
 
字符包含字节 对应数字 备注
 0xxxxxxx  文字符号 0~127  ASCII
 110xxxxx 10xxxxxx  128 ~ 2047  少于128个未使用值
 1110xxxx 10xxxxxx 10xxxxxx  2048 ~ 65535  少于 2048 个未使用值 
 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx   65536 ~ 0x10ffff   其他未使用的值
 
一个字节的字符
c := "z"
b := []byte(c)
fmt.Println("byte:")
fmt.Printf("二进制: %b\n", b)
fmt.Printf("十进制: %d\n", b)
fmt.Printf("十六进制: % x\n\n", b)

r := []rune(c)
fmt.Println("rune:")
fmt.Printf("二进制: %b\n", r)
fmt.Printf("十进制: %d\n", r)
fmt.Printf("十六进制: %x\n", r)
输出
byte:
二进制: [1111010]
十进制: [122]
十六进制: 7a
 
rune:
二进制: [1111010]
十进制: [122]
十六进制: [7a]  
说明
 单字节的字符(ASCII),在两个类型 byte,rune 有相同的输出,对于ASCII,使用 byte 类型更加适合
 
两个字节的字符

c := "È"
b := []byte(c)
fmt.Println("byte:")
fmt.Printf("二进制: %b\n", b)
fmt.Printf("十进制: %d\n", b)
fmt.Printf("十六进制: % x\n\n", b)

r := []rune(c)
fmt.Println("rune:")
fmt.Printf("二进制: %b\n", r)
fmt.Printf("十进制: %d\n", r)
fmt.Printf("十六进制: %x\n", r)
输出
byte:
二进制: [11000011 10001000]
十进制: [195 136]
十六进制: c3 88
 
rune:
二进制: [11001000]
十进制: [200]
十六进制: [c8]
 
说明
为何 byte 与 rune 输出的不一样,先看看调试图

byte 是字节类型,一个由两个字节构成的字符,在数组中,占了两个元素,而 rune 是字符类型,任何一个字符都只占一个数组元素。它们是表示同一个字符,那么它们一定存在某种转换关系。
  二进制 十进制 十六进制
 byte  11000011 10001000 195 136 c3 88
rune 11001000 200 c8
说明 第一个字节去掉打头110
第二个字节去掉打头10
合并两个字节,去掉前面的0
byte二进制就变成rune
byte分别计算每个字节
所以byte两个数字195 136
而且计算时包含了110,10前缀
rune去掉了110,10计算
同理左侧原则
 
11000011 10001000 = 195 136 = c3 88
(11000011 = 195 = c3)
(10001000 = 136 = 88)
11001000 = 200 = c8
上面的等式,读者可以自行验证,它们在一定规则下是相等的,只是表达方式不同而已,所以表格中的 6 个数值是相等的表达
 
三个字节的字符
 
输出
byte:
二进制: [11100100 10111010 10111010]
十进制: [228 186 186]
十六进制: e4 ba ba
 
rune:
二进制: [100111010111010]
十进制: [20154]
十六进制: [4eba]
说明
明白了两个字节的字符的原理,那么三个字节的字符的同理,只是多了一个字节,一个 10 打头的字节(二进制时),而字符的首个字节是 1110 打头,计算方式同理上面的表格介绍。4个字节的字符不再介绍,它并不常见。
 

实例


func main() {
   s := "Hi, 人而"
   fmt.Printf("字节数: %d\n", len(s))
   fmt.Printf("字符数: %d\n\n", utf8.RuneCountInString(s))

   fmt.Printf("%s\t%s\t%s\n", "顺序", "字符", "数值")
   for i, r := range s {
      fmt.Printf("%d\t%q\t%d\n", i, r, r)
   }
}
输出 
字节数: 10
字符数: 6
 
顺序    字符    数值
0       'H'     72
1       'i'     105
2       ','     44
3       ' '     32
4       '人'    20154
7       '而'    32780
说明
len(s) 函数是获取字符串的字节数
RuneCountInString(s) 获取字符串的字符数
for 循环是按字符遍历
Posted by 何敏 on 2020-02-22 07:34:58