详解Go中的rune类型
文章目录
Unicode编码和utf-8编码的关系
需要注意的是,Unicode 只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
比如,汉字’严’的 Unicode 是十六进制数4E25
,转换成二进制数足足有15位(100111000100101
),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。
这里就有两个严重的问题
第一个问题,如何才能区别 Unicode 和 ASCII ? 计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?
第二个问题,我们已经知道,英文字母只用一个字节表示就够了,如果 Unicode 统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
它们造成的结果是:
1)出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode。
2)Unicode 在很长一段时间内无法推广,直到互联网的出现。
utf-8的必要性
互联网的普及,强烈要求出现一种统一的编码方式。UTF-8 就是在互联网上使用最广的一种 Unicode 的实现方式。其他实现方式还包括 UTF-16(字符用两个字节或四个字节表示)和 UTF-32(字符用四个字节表示),不过在互联网上基本不用。这里的关系是,UTF-8 是 Unicode 的实现方式之一。
UTF-8 最大的一个特点,就是它是一种变长的编码方式。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。
UTF-8 的编码规则很简单,只有二条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
下表总结了编码规则,字母x表示可用编码的位。
Unicode符号范围 | UTF-8编码方式
(十六进制) | (二进制)
———————-+———————————————
0000 0000-0000 007F | 0xxxxxxx –> 8位
0000 0080-0000 07FF | 110xxxxx 10xxxxxx –> 16位
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx –> 24位
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx –> 32位
跟据上表,解读 UTF-8 编码非常简单。
如果一个字节的第一位是0,则这个字节单独就是一个字符;如果第一位是1,则连续有多少个1,就表示当前字符占用多少个字节。
下面,还是以汉字严为例,演示如何实现 UTF-8 编码。
‘严’的 Unicode 是4E25
(100111000100101
),根据上表,可以发现4E25
处在第三行的范围内(0000 0800 - 0000 FFFF
),因此’严’的 UTF-8 编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx
。然后,从严的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。
这样就得到了,‘严’的 UTF-8 编码是11100100 10111000 10100101
,转换成十六进制就是E4B8A5
。
rune
类型是什么
rune
类型是 Go 语言的一种特殊数字类型。在 builtin/builtin.go
文件中,它的定义:type rune = int32
;官方对它的解释是:rune
是类型 int32
的别名,在所有方面都等价于它,用来区分字符值跟整数值。使用单引号定义 ,返回采用 UTF-8 编码的 Unicode 码点。Go 语言通过 rune
处理中文,支持国际化多语言。
众所周知,Go 语言有两种类型声明方式:一种叫类型定义声明,另一种叫类型别名声明。其中,别名的使用在大型项目重构中作用最为明显,它能解决代码升级或迁移过程中可能存在的类型兼容性问题。而rune
跟 byte
是 Go 语言中仅有的两个类型别名,专门用来处理字符。当然,我们也可以通过 type
关键字加等号的方式声明更多的类型别名。
rune
类型怎么用
我们知道,字符串由字符组成,字符的底层由字节组成,而一个字符串在底层的表示是一个字节序列。在 Go 语言中,字符可以被分成两种类型处理:对占 1 个字节的英文类字符,可以使用 byte
(或者 unit8
);对占 1 ~ 4 个字节的其他字符,可以使用 rune
(或者 int32
),如中文、特殊符号等。
下面,我们通过示例应用来具体感受一下。
- 统计带中文字符串长度
|
|
前面说到,字符串在底层的表示是一个字节序列。其中,英文字符占用 1 字节,中文字符占用 3 字节,所以得到的长度 14 显然是底层占用字节长度,而不是字符串长度,这时,便需要用到 rune
类型。
|
|
这回对了。很容易,我们解锁了 rune
类型的第一个功能,即统计字符串长度。
- 截取带中文字符串
如果想要截取字符串中 ”Go语言“ 这一段,考虑到底层是一个字节序列,或者说是一个数组,通常情况下,我们会这样:
|
|
结果符合预期。但是,按照字节的方式进行截取,必须预先计算出需要截取字符串的字节数,如果字节数计算错误,就会显示乱码,比如这样:(因为字符串里含有非asccii编码的码点值,所以单个字节的读有时候无法正确读出字符串,会乱码)
|
|
此外,如果截取的字符串较长,那通过字节的方式进行截取显然不是一个高效准确的办法。那有没有不用计算字节数,简单又不会出现乱码的方法呢?不妨试试这样:(针对含有非asccii码的码点,而是在utf-8表里的其他码点,需要2~4个字节才能表述一个正确的符号,例如汉字“烈”的utf-8码值为:28872)
|
|
到这里,我们解锁了 rune
类型的第二个功能,即截取字符串。
为什么 rune
类型可以
通过上面的示例,我们发现似乎在处理带中文的字符串时,都需要用到 rune
类型,这究竟是为什么呢?除了使用 rune
类型,还有其他方法吗?
在深入思考之前,我们需要首先弄清楚 string
、byte
、rune
三者间的关系。
字符串在底层的表示是由单个字节组成的一个不可修改的字节序列,字节使用 UTF-8[1] 编码标识 Unicode[2] 文本。Unicode 文本意味着 .go
文件内可以包含世界上的任意语言或字符,该文件在任意系统上打开都不会乱码。UTF-8 是 Unicode 的一种实现方式,是一种针对 Unicode 可变长度的字符编码,它定义了字符串具体以何种方式存储在内存中。UFT-8 使用 1 ~ 4 为每个字符编码。
Go 语言把字符分 byte
和 rune
两种类型处理。byte
是类型 unit8
的别名,用于存放占 1 字节的 ASCII 字符,如英文字符,返回的是字符原始字节。rune
是类型 int32
的别名,用于存放多字节字符,如占 3 字节的中文字符,返回的是字符 Unicode 码点值。如下图所示:
|
|
它们的对应关系如下图:
可以看到:字符串转化为Unicode编码时,就会将每个字符的Unicode码值打印出来(毕竟Unicode的所有码值涵盖了所有符号和文字);再将Unicode编码转化为utf-8编码时,原来的assiic码值不变,但是超过assiic(即一个字节)可以表示的字符,会拆分成多个字节表示字符。
了解了这些,我们再回过来看看,刚才的问题是不是清楚明白很多?接下来,让我们再来看看源码中是如何处理的,以 utf8.RuneCountInString()[3] 函数为例。
示例:
|
|
源码:
|
|
调用该函数时,传入一个原始的字符串,代码会根据每个字符的码点大小判断是否为 ASCII 字符,如果是,则算做 1 位;如果不是,则查询首字节表,明确字符占用的字节数,验证有效性后再进行计数。
文章出处1
文章作者 cold-bin
上次更新 2022-08-10