Menu

Go rune 与 byte:详解 UTF-8 字符串

Go 中 rune 和 byte 是什么意思,字符串如何以 UTF-8 存储,为什么 len 计算的是字节数,range 如何解码出 rune,string、[]byte 和 []rune 之间如何转换,以及如何使用 bytes 包。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

Go 的字符串是一串字节,而 Go 源代码和字符串字面量都是 UTF-8 编码的。byte 就是其中的一个字节。rune 是一个 Unicode 字符(一个码点),UTF-8 用 1 到 4 个字节来存储它。Go 中关于字符串的大多数困惑,都来自把这两者弄混:

語 在 UTF-8 中占三个字节,所以 "語Go" 是 5 个字节,却只有 3 个 rune。索引循环看到的是五个字节,而第 0、1、2 个字节单独拿出来没有任何意义。range 循环会解码 UTF-8,在索引 0、3 和 4 处产生三个 rune:i 始终是字节偏移量,而不是字符计数。

byte 和 rune 是别名

名称等同于字面量含义
byteuint8byte('A')、0x41一个字节的数据
runeint32'A'、'é'、'世'一个 Unicode 码点

因为它们是别名,byte 就是 uint8,rune 就是 int32;别名和它的底层类型之间不需要转换。单引号表示 rune,双引号表示字符串。'ab' 会导致编译错误,因为一个 rune 字面量只能包含一个字符。

rune 是数字,所以可以对它们做算术运算:

'7' - '0' 是把数字字符转换成对应数值的经典写法。unicode 包可以对 rune 分类(IsLetter、IsDigit、IsSpace、IsUpper),并能正确地转换非 ASCII 字母的大小写。%c 把 rune 打印成字符,%U 打印成 U+4E16 的形式,%q 打印成带引号的 rune 字面量。

UTF-8 如何存储字符

码点范围字节数示例
U+0000 至 U+007F1ASCII:a、7、{
U+0080 至 U+07FF2é、ß、ж、ع
U+0800 至 U+FFFF3語、€、한
U+10000 及以上4emoji、生僻的中日韩字符

ASCII 文本在 UTF-8 中完全不变,这就是为什么按字节处理的代码在测试时常常没问题,一遇到第一个带重音符号的名字就出错。文本转二进制工具可以显示你粘贴的任何文本的 UTF-8 字节。unicode/utf8 包可以直接处理这种编码:

% x(中间有一个空格)以十六进制打印字节,并用空格分隔。utf8.ValidString 报告字符串是否是合法的 UTF-8。Go 字符串可以包含任意字节,不一定是合法的 UTF-8;range 遇到非法字节时,会产生 utf8.RuneError(U+FFFD,即替换字符),并前进一个字节。

string、[]byte 和 []rune 之间的转换

转换结果开销
[]byte(s)UTF-8 字节复制
[]rune(s)解码后的码点解码并复制(每个 rune 4 字节)
string(b)由字节构成的字符串复制
string(r),r 为 []rune编码为 UTF-8复制
string(r),r 为 rune只有一个字符的字符串很小

按字节反转会把 世界 打乱成非法的 UTF-8,所以 reverse 是按 rune 处理的。即使是 rune 也不能说明全部问题:像 é 这样的字符也可以写成 e 加上一个组合重音符号,也就是两个 rune。对于用户看到的字符(字素簇),请使用 github.com/rivo/uniseg 这样的库。

每次转换都会复制,因为字符串是不可变的,而 slice 是可变的。在热点代码中,避免来回转换;编译器会优化某些情况(例如把 string(b) 用作 map 的键或用于比较)来省去复制。

索引得到的是字节

s[i] 返回一个 byte,s[i:j] 按字节偏移量切片:

s := "café"
fmt.Println(s[3])            // 195: the first byte of é
fmt.Println(string(s[3]))    // "Ã": that byte read as a code point
fmt.Println(s[:3])           // "caf"
fmt.Println(s[:4])           // "caf\xc3": half of é, invalid UTF-8

string(s[3]) 是一个陷阱:把单个字节转换为 string,会把它当作码点 195,也就是 Ã,而不是那个字节本身。要找到字符的边界,使用 range、strings.Index 系列函数(它们返回的字节偏移量总是落在字符边界上),或者 utf8.DecodeRuneInString。

bytes 包

bytes 为 []byte 提供了与 strings 对应的函数:bytes.Contains、bytes.Split、bytes.Fields、bytes.TrimSpace、bytes.Equal、bytes.ToUpper 等等。当数据以字节的形式到来时(文件内容、HTTP 请求体、网络缓冲区),就用它,这样不必先转成字符串再转回来:

bytes.Buffer 是一个可增长的字节缓冲区,实现了 io.Reader 和 io.Writer。既要写入又要读取字节时,它是合适的工具;如果只是构建字符串,strings.Builder 的开销略小一些。比较字节 slice 要用 bytes.Equal,因为 == 不能用于 slice。

如何选择

  • 要显示、比较或搜索的文本:保持为 string,需要逐个字符遍历时使用 range。
  • 字符位置、反转、截取前 n 个字符:转换成 []rune。
  • I/O、哈希、编码、二进制协议:使用 []byte 和 bytes 包。
  • 单个字符:用 rune。单个原始字节:用 byte。

常见问题

Go 中的 rune 是什么?

rune 是 int32 的别名,表示一个 Unicode 码点。rune 字面量用单引号书写:'a' 是 97,'é' 是 233,'世' 是 19990。用 for i, r := range s 遍历字符串,得到的就是从字符串的 UTF-8 字节解码出的 rune。

Go 中 rune 和 byte 有什么区别?

byte(uint8 的别名)是 8 位原始数据。rune(int32 的别名)是一个完整的 Unicode 字符。在 UTF-8 字符串中,每个 ASCII 字符占一个字节,其他字符占两到四个字节,但仍然是一个 rune。len(s) 计算字节数;utf8.RuneCountInString(s) 计算 rune 数。

Go 中如何遍历字符串的每个字符?

使用 for i, r := range s。每次迭代得到字节偏移量 i 和 rune r,所以多字节字符能被正确处理。普通的索引循环 for i := 0; i < len(s); i++ 访问的是字节,会把非 ASCII 字符拆成碎片。

Go 中如何把字符串转换成字节 slice?

b := []byte(s) 会把字符串的字节复制到一个新的 slice 中。string(b) 再转换回来,同样会复制一次。当你需要按字符处理时,比如反转字符串或取前 n 个字符,应改用 []rune(s)。

Coddy 编程语言插图

和 Coddy 一起学编程

开始