背景:在我们使用 Golang 进行开发过程中,总是绕不开对字符或字符串的处理,而在 Golang 语言中,对字符和字符串的处理方式可能和其他语言不太一样,比如 Python 或 Java 类的语言,本篇文章分享一些 Golang 语言下的 Unicode 和字符串编码。

Go 语言字符编码

注意

被转换的整数值应该可以代表一个有效的 Unicode 代码点,否则转换的结果就将会是 "�",即:一个仅由高亮的问号组成的字符串值。

另外,当一个 string 类型的值被转换为 [] rune 类型值的时候,其中的字符串会被拆分成一个一个的 Unicode 字符。

显然,Go 语言采用的字符编码方案从属于 Unicode 编码规范。更确切地说,Go 语言的代码正是由 Unicode 字符组成的。Go 语言的所有源代码,都必须按照 Unicode 编码规范中的 UTF-8 编码格式进行编码。

换句话说,Go 语言的源码文件必须使用 UTF-8 编码格式进行存储。如果源码文件中出现了非 UTF-8 编码的字符,那么在构建、安装以及运行的时候,go 命令就会报告错误 "illegal UTF-8 encoding"。

ASCII 编码

ASCII 编码方案使用单个字节(byte)的二进制数来编码一个字符。标准的 ASCII 编码用一个字节的最高比特(bit)位作为奇偶校验位,而扩展的 ASCII 编码则将此位也用于表示字符。ASCII 编码支持的可打印字符和控制字符的集合也被叫做 ASCII 编码集。

我们所说的 Unicode 编码规范,实际上是另一个更加通用的、针对书面字符和文本的字符编码标准。它为世界上现存的所有自然语言中的每一个字符,都设定了一个唯一的二进制编码。

它定义了不同自然语言的文本数据在国际间交换的统一方式,并为全球化软件创建了一个重要的基础。

Unicode 编码规范以 ASCII 编码集为出发点,并突破了 ASCII 只能对拉丁字母进行编码的限制。它不但提供了可以对世界上超过百万的字符进行编码的能力,还支持所有已知的转义序列和控制代码。

我们都知道,在计算机系统的内部,抽象的字符会被编码为整数。这些整数的范围被称为代码空间。在代码空间之内,每一个特定的整数都被称为一个代码点。

一个受支持的抽象字符会被映射并分配给某个特定的代码点,反过来讲,一个代码点总是可以被看成一个被编码的字符。

Unicode 编码规范通常使用十六进制表示法来表示 Unicode 代码点的整数值,并使用 “U+” 作为前缀。比如,英文字母字符 “a” 的 Unicode 代码点是 U+0061。在 Unicode 编码规范中,一个字符能且只能由与它对应的那个代码点表示。

Unicode 编码规范现在的最新版本是 11.0,并会于 2019 年 3 月发布 12.0 版本。而 Go 语言从 1.10 版本开始,已经对 Unicode 的 10.0 版本提供了全面的支持。对于绝大多数的应用场景来说,这已经完全够用了。

UTF-8UTF-16UTF-32

在这几种编码格式的名称中,“-” 右边的整数的含义是,以多少个比特位作为一个编码单元。以 UTF-8 为例,它会以 8 个比特,也就是一个字节,作为一个编码单元。并且,它与标准的 ASCII 编码是完全兼容的。也就是说,在 [0x00, 0x7F] 的范围内,这两种编码表示的字符都是相同的。这也是 UTF-8 编码格式的一个巨大优势。

UTF-8 是一种可变宽的编码方案。换句话说,它会用一个或多个字节的二进制数来表示某个字符,最多使用四个字节。比如,对于一个英文字符,它仅用一个字节的二进制数就可以表示,而对于一个中文字符,它需要使用三个字节才能够表示。不论怎样,一个受支持的字符总是可以由 UTF-8 编码为一个字节序列。以下会简称后者为 UTF-8 编码值。

string 类型的底层存储

字符字节
runebyte
runeUnicode 字符
runeint32
UTF-8 编码Unicode 字符[1, 4] 范围内的字节序列

我们可以看如下代码:

对应输出的效果如下:

[]runeruneruneUTF-84eba751f人生
e4 ba bae7 94 9fUFT-84eba751f人生
注意:

我们对上述字符串的底层编码进行图形拆解:

转换为 UTF-8 编码值字节序列表达和存储

所以,一个 string 类型的值在底层就是一个能够表达若干个 UTF-8 编码值的字节序列。

range 遍历字符串示例

注意:字节序列

我们来看如下代码:

使用 for range 进行遍历字符串,得到如下结果:

可以看到,遍历字符串中的每个字符时,对应的表示方式和我们上图中分析的是一致的,但是你有没有发现一个小问题呢?

生710人[e4 ba ba]生10

所以,需要注意的是: for range 语句可以逐一的迭代出字符串值里的每个 Unicode 字符,但是相邻的 Unicode 字符的索引值并不一定是连续的,这取决于前一个 Unicode 字符是否为单字节字符。