在Go语言中没有字符类型,字符只是整数的特殊用例,使用了
byte
和rune
作为别名Go的字符串使用了UTF-8的编码来表示,所以要明确好Unicode码和ASCII码的区别
如何使用Go来遍历字符串、修改字符串,这也是一个常见的问题
Github issues:https://github.com/littlejoyo/Blog/issues/
1.Go的byte和rune
Go的源码表示
1 | // byte is an alias for uint8 and is equivalent to uint8 in all ways. It is |
由上可知:
byte
是uint8
的别名,长度为 1 个字节,可以表示 2^8 = 255 个字符,在Go中用于表示 ASCII 字符rune
是int32
的别名,长度为 4 个字节,可以表示 2^32个字符,用于表示以 UTF-8 编码的 Unicode 码点
2.ASCII、Unicode 和 UTF-8字符的区别
2.1 ASCII码
ASCII
使用了一个字节实现对英语字符与二进制位之间的关系,做了统一规定每一个二进制位(bit)有
0
和1
两种状态,又因为一个字节等于8位所以,八个二进制位就可以组合出2^8 = 256种状态,足够表示255个字符。
英语用128个符号编码就够了,但是用来表示其他语言,128个符号是不够的。
比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示,因此各个国家开始定制各种可以全面表示各国语言的编码,造成了世界上存在着多种编码方式的现象,如果不选择正确的编码方式就会出现乱码。
2.2 Unicode
可以想象,如果有一种编码,将世界上所有的符号都纳入其中。每一个符号都给予一个独一无二的编码,那么乱码问题就会消失,所以
Unicode
码出现了。Unicode
当然是一个很大的集合,现在的规模可以容纳100多万个符号。每个符号的编码都不一样,比如,U+0639
表示阿拉伯字母Ain
,U+0041
表示英语的大写字母A
,U+4E25
表示汉字严
。Unicode
编码也存在问题,因为Unicode
只是一个符号集,它只规定了符号的二进制代码,却没有规定这个二进制代码应该如何存储。
两个严重的问题:
第一个问题是,
如何才能区别 Unicode 和 ASCII
?计算机怎么知道三个字节表示一个符号,而不是分别表示三个符号呢?第二个问题是,
会出现明显浪费存储的问题
,英文字母只用一个字节表示就够了,如果Unicode
统一规定,每个符号用三个或四个字节表示,那么每个英文字母前都必然有二到三个字节是0,这对于存储来说是极大的浪费,文本文件的大小会因此大出二三倍,这是无法接受的。
这两个问题造成的结果就是:
出现了 Unicode 的多种存储方式,也就是说有许多种不同的二进制格式,可以用来表示 Unicode。
Unicode 在很长一段时间内无法推广,直到互联网的出现。
2.3 UTF-8
互联网的普及,强烈要求出现一种统一的编码方式。
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 |
0000 0080-0000 07FF | 110xxxxx 10xxxxxx |
0000 0800-0000 FFFF | 1110xxxx 10xxxxxx 10xxxxxx |
0001 0000-0010 FFFF | 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx |
如果一个字节的第一位是
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
。
参考链接:阮一峰:字符编码笔记:ASCII,Unicode 和 UTF-8
3.Go 语言中表示字符呢?
在 Go 语言中使用单引号包围来表示字符,例如 ‘a’。
byte
如果要表示 byte 类型的字符,可以使用 byte
关键字来指明字符变量的类型:
直接输出的话会输出字符对应的ASCII码
如果想要输出具体字符,需要格式化说明符
%c
来输出
代码如下:
1 | var b byte = 'A' |
rune
与 byte 相同,想要声明 rune 类型的字符可以使用 rune
关键字指明:
注:如果在声明一个字符变量时没有指明类型,Go 会默认它是 rune
类型
1 | var b rune = 'A' |
4.Go为什么需要两种类型?
在 Go 语言中,使用的是 UTF-8
编码,用 UTF-8
编码来存放一个 ASCII
字符依然只需要一个字节,而存放一个非 ASCII
字符,则需要 2个、3个、4个字节,它是不固定的。
byte 占用一个字节,因此它可以用于表示 ASCII 字符。
rune占用4个字节,可以用它表示 UTF-8 字符,因为UTF-8 是一种变长的编码方法,字符长度从 1 个字节到 4 个字节不等。
所以:
Go 中的字符串存放的是 UTF-8 编码,那么我们使用 s[i] 这样的下标方式获取到的内容就是 UTF-8 编码中的一个字节。
对于非 ASCII 字符而言,这样的一个字节没有实际的意义,除非你想编码或解码 UTF-8 字节流。
在 Go 语言中,已经有很多现成的方法来编码或解码 UTF-8 字节流了。
1 | str := "你好,世界" |
输出结果:
1 | � |
上面的问题,如何单独截取字符呢?
利用 []rune()
将字符串转为 Unicode 码点再进行截取,这样就无需考虑字符串中含有 UTF-8 字符的情况了
1 | testString := "你好,世界" |
输出结果:
1 | 第一个字符:你 |
5.遍历字符串
字符串遍历有两种方式,一种是下标遍历,一种是使用 range。
5.1 下标遍历
由于在 Go 语言中,字符串以
UTF-8
编码方式存储,使用len()
函数获取字符串长度时,注意获取到的是UTF-8
编码字符串的字节长度通过下标索引获取值将会产生一个字节,所以,如果字符串中含有非ASCII编码字符,就会出现乱码,例如中文字符需要1~4不等的字节来表示。
例如,遍历 “Hello,世界”
1 | s := "Hello,世界" |
输出结果:
1 | H 的类型是 uint8 |
逗号和中文字符就出现了乱码,因为属于非ASCII码,它们不能用ASCII码表示。
另外说明了使用 s[i] 这样的下标方式获取到的内容就是 UTF-8 编码中的一个字节,无法使用byte类型表示,否则出现乱码
如果想要正确表示中文字符,需要使用rune类型,才能对字符进行存储和表示
通过rune
类型使用下标遍历:
1 | s := "Hello,世界" |
输出结果:
1 | H 的类型是 int32 |
5.2 range遍历
1 | s := "Hello,Go语言" |
输出结果:
1 | H 的类型是 int32 |
6.字符串的修改
在 Go 语言中,字符串的内容是不能修改的,也就是说,你不能用 s[i] 这种方式修改字符串中的 UTF-8 编码
如果你一定要修改,那么你可以将字符串的内容复制到一个
可写的缓冲区
中,然后再进行修改。这样的缓冲区一般是
[]byte
或[]rune
。如果要对字符串中的字节进行修改,则转换为[]byte
格式,如果要对字符串中的字符进行修改,则转换为[]rune
格式,转换过程会自动复制数据。
使用用 []byte修改字符串中的字节:
1 | s := "Hello,Go语言" |
输出结果:
1 | Hello,Go语言 |
转换为[]byte修改的是字符串的字节,一般不常用,因为 []byte 代表的是字节数组,每个元素都是字节而不是字符。
使用用 []rune修改字符串中的字符:
1 | s := "Hello,Go语言" |
输出结果:
1 | Hello,Go语言 |
验证了无法直接在原来的字符串上进行字符的修改,需要再缓冲区额外修改。
在 []byte 中处理 Rune 字符(需要用到 utf8 包中的解码函数)
1 | func main { |
输出结果:
1 | H |
总结:
Go 语言中没有字符的概念,一个字符就是一堆字节,它可能是单个字节(ASCII 字符集),也有可能是多个字节(Unicode 字符集)
byte
是 uint8 的别名,长度为 1 个字节,用于表示 ASCII 字符rune
则是 int32 的别名,长度为 4 个字节,用于表示以 UTF-8 编码的 Unicode 码点字符串的截取是以字节为单位的,使用下标索引字符串只能获取到字节,获取字符需要进行准换
想要遍历
rune
类型的字符则使用range
方法进行遍历。
微信公众号
扫一扫关注Joyo说公众号,共同学习和研究开发技术。