目前对字符集、字符编码、编码规则等概念分得还是不太清,可能术语方面会不太准确,有待加强认识
计算机内部的所有信息都是二进制形式的,二进制对计算机来说最简单的,但却并不人类友好。于是美国人制定了 ASCII 码作为二进制和英文字符的映射。但随着计算机在世界范围内的流行,ASCII 码的容量无法满足各国的需求;很多国家都开发了自己独有的字符编码方式,但是在拥有不同编码方式的计算机之间通信,会造成乱码。各国间的通信促使了统一字符编码的出现,这正是 Unicode。
Unicode
Unicode 是一个包含了世界上通用的符号集合,不管是英文还是中文,每个字符在 Unicode 都有唯一的编码。但 Unicode 仅仅是一个字符集,它只规定了符号的二进制编码,而没有规定应该如何存储这个二进制编码。比如汉
字,Unicode 编码是6C49
,转成二进制格式是110110001001001
,15 位,至少占用 2 个字节;而字母a
的 Unicode 编码是0061
,转为二进制格式是1100001
,7 位,至少占用 1 个字节。
UTF-8 编码规则
UTF-8 是一种变长的编码方式,它可以使用 1-4 个字节来表示一个符号,根据不同的符号而采取不同的字节长度。由于 Unicode 字符集并没有填满,因此,可以用 UTF-8 来编码,假如以后填满了,说不定就得发明一种新的编码方式来存储。
UTF-8 的编码规则很简单,只有二条:
-
对于单字节的符号,字节的第一位设为 0,后面 7 位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
-
对于 n 字节的符号(n > 1),第一个字节的前 n 位都设为 1,第 n+1 位设为 0,后面字节的前两位一律设为 10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
由此得出不同字节长度 UTF-8 编码的范围(计算所有 x 位组成的最大最小值):
字节长度 | Unicode 范围 | UTF-8 编码二进制形式 |
---|---|---|
1 | 0000 0000-0000 007F | 0xxxxxxxx |
2 | 0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
3 | 0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
4 | 0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
目前 Unicode 字符分为 17 组编排,0x0000 至 0x10FFFF,每组称为平面(Plane)。目前只用了少数平面。
Unicode 与 UTF-8 编码对照
Go 语言中 rune 表示 Unicode 码点。第 10 行代码打印的正是汉
字对应的 Unicode 十六进制编码,结果是U+6C49
,第 11 行是其二进制编码;第 13、14 行将汉
写入内存中,这时候汉
字在内存中以 UTF-8 方案进行编码,因此,第 16、17 行打印出来的结果跟第 10、11 行不一样。我们可以对比一下汉
的 Unicode 码和 UTF-8 编码的二进制形式,分别是:110 1100 0100 1001
和11100110 10110001 10001001
。通过上表可以判断汉
字的 UTF-8 编码为 3 个字节,将 Unicode 码按照该方式转为 UTF-8 编码,如下:
0110 110001 001001
11100110 10110001 10001001
也可以通过 Go 代码来实现。
package main
import (
"bytes"
"fmt"
)
func main() {
var r rune = '汉'
fmt.Printf("%U\n", r) // U+6C49
fmt.Printf("%b\n", r) // 110 1100 0100 1001
buf := bytes.NewBuffer(nil)
buf.WriteRune(r)
fmt.Printf("%U\n", buf.Bytes()) // E6B189
fmt.Printf("%b\n", buf.Bytes()) // [11100110 10110001 10001001]
fmt.Printf("%s\n", buf.Bytes()) // 汉
}
同理,也可以通过 UTF-8 的编码规则反推出 Unicode 码,主要是通过判断字节的前面四位来推断字节长度,这里用到了状态机(随便了解过一下,大概知道在这可以用),大概步骤:第 1 个字节第 1 位是 0,则 UTF-8 编码占 1 字节;否则,字节长度超过1,如果第 2 个字节是 1 且第 3 个字节是 0,则占 2 个字节;否则,一直判断,直到第 5 位,如此而已,当然编码会麻烦点。