
3.3 字符串、字符和字节
我们已经讨论过切片了,接下来讨论字符串。你可能会认为Go中的字符串是由字符构成的,但事实并非如此。实际上Go使用一系列字节来表示字符串。这些字节并不一定使用特定的字符编码,但是有些Go标准库中的函数(以及我们会在下一章讨论的for-range
循环)会假设字符串是由一系列UFT-8编码构成的。
依照语言规范,Go的源代码编码格式始终为UTF-8。除非对字符串字面量使用十六进制转义,否则字符串字面量都是UTF-8格式。
与从数组或切片获取单个值的方式一样,通过索引表达式也可以从字符串中获取单个值:

与数组和切片一样,字符串的索引也是从零开始的。在这个代码示例中,b
被赋值为s
的第7个值,也就是t
。
在数组和切片中使用的派生表达式也适用于字符串:

这会将“o t”赋值给s2
,将“Hello”赋值给s3
,将“there”赋值给s4
。
虽然在Go中能方便地使用切片方式创建子字符串,并且使用索引获取部分数据,但操作时要仔细。字符串是不可变的,它并不会像切片和子切片一样有更新所导致的问题。不过有另外的问题,字符串是由一系列字节组成的,但是UTF-8的码位的长度为1~4个字节。我们之前的例子中的字符串都是由1个字节长的UTF-8码位组成的,所以结果符合预期。但是处理非英语或者表情符号时,你会遇到多字节UTF-8码位的情况:

在这个例子中,s3
还是等于“Hello”。变量s4
设置为太阳的表情。但是s2
并不是“o”,而是“o
”。这是因为我们只是拷贝了太阳表情码位的第一个字节,所以是无效的。
Go支持将一个字符串传递给内置的len
函数以求得字符串的长度。由于字符串索引和切片表达式以字节为单位计算位置,所以返回的结果以字节为单位,而不是以码位为单位:

以上代码输出10,而不是7,这是因为太阳表情在UTF-8中是由4个字节表示的。
虽然Go中支持对字符串使用派生和索引的用法,但是最好在已知字符串中的字符都仅占用1个字节时使用。
正因为字符、字符串和字节之间的复杂关系,Go在这些类型中包含一系列有趣的类型转换。一个字符或者字节可以转换为一个字符串:

初学者经常犯的错误就是尝试将一个整型转换为一个字符串:

y
的结果是“A”而不是“65”。从Go 1.15开始,go vet
会阻止非字符或者字节类型转换为字符串类型。
字符串可以在字节类型切片或者字符类型切片中来回转换。尝试在The Go Playground(https://oreil.ly/N7fOB)中执行示例3-9。
示例3-9:将字符串转换为切片

执行代码后会得到以下结果:

第一行结果将字符串转换为一组UTF-8字节。第二行则将字符串转换为一组字符。
Go中的大部分数据都是作为字节序列进行读写的,所以一般都在字符串切片和字节切片之间进行转换。字符切片并不常见。
UTF-8
UTF-8是Unicode中最常用的一种编码。Unicode使用4个字节(32比特)来表示一个码位(字符和修饰符的术语名)。因此表示Unicode的最简单方式就是每个码位使用4个字节,称为UTF-32。由于浪费了大量的空间,所以这种方式并不常见。在Unicode的实现细节中,32比特中的11比特总是为零。另外一种常见的编码是UTF-16,使用一个或者两个16比特(2字节)序列表示每一个码位。这也比较浪费,世界上的大部分内容都是使用单个字节的码位编写的,这也是UTF-8的由来。
UTF-8编码很巧妙。它允许使用单个字节来表示所有Unicode中值小于128的字符(包含所有的字母、数字和英文中常用的标点),但是可以扩展为至多4个字节以表示更大的数。结果就是UTF-8在最坏的情况下和UTF-32一样。UTF-8还有一些其他良好的特性。与UTF-32和UTF-16不同,你不需要关心大端小端的问题。并且在查看序列中的任何一个字节时都能够明确知道是处于UTF-8的起始处还是中间段。这也意味着不存在误读字符的问题。
使用UTF-8唯一的缺点是不能随意地访问使用UTF-8编码的字符串。虽然可以知道是否使用了字符,但是并不能确定这个字符使用了多少字节,因此需要从字符串的开头开始计数。Go并不限定字符串一定使用UTF-8进行编码,但是强烈建议使用这种方式。我们在第4章会看到如何处理UTF-8编码的字符串。
有趣的事实:UTF-8是由Ken Thompson和Rob Pike在1992发明的,他们也是Go的缔造者。
相对于对字符串使用切片和索引表达式,使用标准库的strings
和unicode/utf8
库中的函数处理子字符串和码位是更好的选择。在下一章中,我们会看到如何使用for-range
循环来遍历字符串中的码位。