背景:在咱们使用 Golang 进行开发过程当中,老是绕不开对字符或字符串的处理,而在 Golang 语言中,对字符和字符串的处理方式可能和其余语言不太同样,好比 Python 或 Java 类的语言,本篇文章分享一些 Golang 语言下的 Unicode 和字符串编码。git
注意
: 在 Golang 语言中的标识符能够包含 "任何 Unicode 编码能够标识的字母字符"。github
被转换的整数值应该能够表明一个有效的 Unicode 代码点,不然转换的结果就将会是 "�",即:一个仅由高亮的问号组成的字符串值。golang
另外,当一个 string 类型的值被转换为 [] rune 类型值的时候,其中的字符串会被拆分红一个一个的 Unicode 字符。web
显然,Go 语言采用的字符编码方案从属于 Unicode 编码规范。更确切地说,Go 语言的代码正是由 Unicode 字符组成的。Go 语言的全部源代码,都必须按照 Unicode 编码规范中的 UTF-8 编码格式进行编码。编辑器
换句话说,Go 语言的源码文件必须使用 UTF-8 编码格式进行存储。若是源码文件中出现了非 UTF-8 编码的字符,那么在构建、安装以及运行的时候,go 命令就会报告错误 "illegal UTF-8 encoding"。ui
ASCII 编码方案使用单个字节(byte)的二进制数来编码一个字符。标准的 ASCII 编码用一个字节的最高比特(bit)位做为奇偶校验位,而扩展的 ASCII 编码则将此位也用于表示字符。ASCII 编码支持的可打印字符和控制字符的集合也被叫作 ASCII 编码集。编码
咱们所说的 Unicode 编码规范,其实是另外一个更加通用的、针对书面字符和文本的字符编码标准。它为世界上现存的全部天然语言中的每个字符,都设定了一个惟一的二进制编码。spa
它定义了不一样天然语言的文本数据在国际间交换的统一方式,并为全球化软件建立了一个重要的基础。翻译
Unicode 编码规范以 ASCII 编码集为出发点,并突破了 ASCII 只能对拉丁字母进行编码的限制。它不但提供了能够对世界上超过百万的字符进行编码的能力,还支持全部已知的转义序列和控制代码。code
咱们都知道,在计算机系统的内部,抽象的字符会被编码为整数。这些整数的范围被称为代码空间。在代码空间以内,每个特定的整数都被称为一个代码点。
一个受支持的抽象字符会被映射并分配给某个特定的代码点,反过来说,一个代码点老是能够被当作一个被编码的字符。
Unicode 编码规范一般使用十六进制表示法来表示 Unicode 代码点的整数值,并使用 “U+” 做为前缀。好比,英文字母字符 “a” 的 Unicode 代码点是 U+0061。在 Unicode 编码规范中,一个字符能且只能由与它对应的那个代码点表示。
Unicode 编码规范如今的最新版本是 11.0,并会于 2019 年 3 月发布 12.0 版本。而 Go 语言从 1.10 版本开始,已经对 Unicode 的 10.0 版本提供了全面的支持。对于绝大多数的应用场景来讲,这已经彻底够用了。
Unicode 编码规范提供了三种不一样的编码格式,即:UTF-8
、UTF-16
和 UTF-32
。其中的 UTF 是 UCS Transformation Format 的缩写。而 UCS 又是 Universal Character Set 的缩写,但也能够表明 Unicode Character Set。因此,UTF 也能够被翻译为 Unicode 转换格式。它表明的是字符与字节序列之间的转换方式。
在这几种编码格式的名称中,“-” 右边的整数的含义是,以多少个比特位做为一个编码单元。以 UTF-8 为例,它会以 8 个比特,也就是一个字节,做为一个编码单元。而且,它与标准的 ASCII 编码是彻底兼容的。也就是说,在 [0x00, 0x7F] 的范围内,这两种编码表示的字符都是相同的。这也是 UTF-8 编码格式的一个巨大优点。
UTF-8 是一种可变宽的编码方案。换句话说,它会用一个或多个字节的二进制数来表示某个字符,最多使用四个字节。好比,对于一个英文字符,它仅用一个字节的二进制数就能够表示,而对于一个中文字符,它须要使用三个字节才可以表示。不论怎样,一个受支持的字符老是能够由 UTF-8 编码为一个字节序列。如下会简称后者为 UTF-8 编码值。
在 Go 语言中,一个 string 类型的值既能够被拆分为一个包含多个字符
的序列,也能够被拆分为一个包含多个字节
的序列。
前者能够由一个以 rune
为元素类型的切片来表示,然后者则能够由一个以 byte
为元素类型的切片表明。
rune
是 Go 语言特有的一个基本数据类型,它的一个值就表明一个字符,即:一个 Unicode 字符
(再通俗点,就是一个中文字符,占 3byte)。
从 Golang 语言的源码 (https://github.com/golang/go/blob/master/src/builtin/builtin.go#L92) 中咱们其实能够知道,rune
类型底层实际上是一个 int32
类型。
咱们已经知道,UTF-8 编码
方案会把一个 Unicode 字符
编码为一个长度在 [1, 4] 范围内的字节序列
,也就是说,一个 rune 类型的值会由四个字节宽度的空间来存储。它的存储空间老是可以存下一个 UTF-8 编码值。
咱们能够看以下代码:
func unicodeAndUtf8() {
tempStr := "BGBiao 的SRE人生." fmt.Printf("string:%q\n",tempStr) fmt.Printf("rune(char):%q\n",[]rune(tempStr)) fmt.Printf("rune(hex):%x\n",[]rune(tempStr)) fmt.Printf("bytes(hex):% x\n",[]byte(tempStr)) } 复制代码
对应输出的效果以下:
string:"BGBiao 的SRE人生."
rune(char):['B' 'G' 'B' 'i' 'a' 'o' ' ' '的' 'S' 'R' 'E' '人' '生' '.'] rune(hex):[42 47 42 69 61 6f 20 7684 53 52 45 4eba 751f 2e] bytes(hex):42 47 42 69 61 6f 20 e7 9a 84 53 52 45 e4 ba ba e7 94 9f 2e 复制代码
第二行输出能够看到字符串在被转换为 []rune
类型的值时,其中每一个字符都会成为一个独立的 rune
类型的元素值。而每一个 rune
底层的值都是采用 UTF-8
编码值来表达的,因此第三行的输出,咱们采用 16 进制数来表示上述字符串,每个 16 进制的字符分别表示一个字符,咱们能够看到,当遇到中文字符时,因为底层存储须要更大的空间,因此使用的 16 进制数字也比较大,好比 4eba
和 751f
分别表明人
和生
。
但其实,当咱们将整个字符的 UTF-8 编码值都拆成响应的字节序列时,就变成了第四行的输出,能够看到一个中文字符其实底层是占用了三个 byte,好比 e4 ba ba
和 e7 94 9f
分别对应 UFT-8
编码值的 4eba
和 751f
,也即中文字符中的人
和生
。
注意:
对于一个多字节的 UTF-8 编码值来讲,咱们能够把它当作一个总体转换为单一的整数,也能够先把它拆成字节序列,再把每一个字节分别转换为一个整数,从而获得多个整数。
咱们对上述字符串的底层编码进行图形拆解:
总之,一个 string 类型的值会由若干个 Unicode 字符组成,每一个 Unicode 字符均可以由一个 rune 类型的值来承载。这些字符在底层都会被转换为 UTF-8 编码值
,而这些 UTF-8 编码值又会以字节序列
的形式表达和存储
。
因此,一个 string 类型的值在底层就是一个可以表达若干个 UTF-8 编码值的字节序列。
注意:
带有 range 子句的 for 语句会先把被遍历的字符串值拆成一个字节序列
,而后再试图找出这个字节序列中包含的每个 UTF-8 编码值,或者说每个 Unicode 字符。所以在 range for 语句中,赋给第二个变量的值是 UTF-8 编码值表明的那个 Unicode 字符,其类型会是 rune。
咱们来看以下代码:
func rangeString() {
tempStr := "BGBiao 人生"
for k,v := range tempStr {
fmt.Printf("%d : %q %x [% x]\n",k,v,[]rune(string(v)),[]byte(string(v)))
}
}
复制代码
使用 for range 进行遍历字符串,获得以下结果:
0 : 'B' [42] [42]
1 : 'G' [47] [47]
2 : 'B' [42] [42]
3 : 'i' [69] [69]
4 : 'a' [61] [61]
5 : 'o' [6f] [6f]
6 : ' ' [20] [20]
7 : '人' [4eba] [e4 ba ba]
10 : '生' [751f] [e7 94 9f]
复制代码
能够看到,遍历字符串中的每一个字符时,对应的表示方式和咱们上图中分析的是一致的,可是你有没有发现一个小问题呢?
即在遍历过程当中,最后一个字符生
的索引一下从 7
变成了 10
,这是由于人
这个字符底层是由三个字节共同表达的,即 [e4 ba ba]
,所以下一个字符的索引值就须要加 3,而生
的索引值也就变成了 10
而不是 8。
因此,须要注意的是: for range 语句能够逐一的迭代出字符串值里的每一个 Unicode 字符,可是相邻的 Unicode 字符的索引值并不必定是连续的,这取决于前一个 Unicode 字符是否为单字节字符。
本文使用 mdnice 排版