Golang byte/rune/string
byte、rune 和 string
在 Go 的源码中,byte 和 rune 都是已有类型的别名:
type byte = uint8 // byte 就是 uint8
type rune = int32 // rune 就是 int32
注意:它们不是新类型。Go 定义新类型时不使用
=。
string 则表示一段不可变的字节序列。Go 本身并不要求字符串必须采用 UTF-8 编码,但在处理文本时,通常使用 UTF-8。
从存储大小来看,byte 占 1 个字节,rune 占 4 个字节,string 的长度不固定。但更重要的是它们各自表达的语义:
byte表示一个原始字节,不关心该字节对应文本还是二进制数据;rune表示一个 Unicode 码点,例如'中'、'😀';string表示一段不可变的字节序列,通常用于存储和处理文本。
字符 != 字节
对于 ASCII 字符,一个字符恰好占一个字节,因此容易将“字节”和“字符”混为一谈。但在 UTF-8 编码下,一个 Unicode 码点可能由多个字节表示。
例如:
s := "a中b"
fmt.Println(len(s)) // 输出 5,而不是 3
len() 计算的是字符串的字节数。其中 a 和 b 各占 1 个字节,中 的 UTF-8 编码占 3 个字节,因此总长度为 5。
如果需要统计 Unicode 码点的数量,可以转换为 []rune:
fmt.Println(len([]rune(s))) // 输出 3
需要注意,rune 对应的是 Unicode 码点,并不严格等同于用户视觉上看到的一个“字符”。例如部分 emoji 和带组合符号的文字可能由多个码点组成。
字符串遍历的两种方式
Go 中遍历字符串常见的方式有两种,它们处理的维度不同。
第一种是使用索引遍历:
s := "a中b"
for i := 0; i < len(s); i++ {
fmt.Printf("%d ", s[i])
}
// 输出:97 228 184 173 98
s[i] 得到的是 byte。因此,中 的 UTF-8 编码会被拆成 228 184 173 三个字节,一共输出 5 个数字。
通过这种方式无法直接按 Unicode 码点读取多字节字符,只能逐个读取其底层字节。
第二种是使用 range 遍历:
for i, r := range s {
fmt.Printf("(索引%d: %c) ", i, r)
}
// 输出:(索引0: a) (索引1: 中) (索引4: b)
此时 r 的类型是 rune,表示解码后的 Unicode 码点。
同时需要注意索引 i:它依次为 0、1、4,而不是 0、1、2。这是因为 i 表示当前 rune 在原字符串中的字节偏移量。中 占用索引 1、2、3 三个字节,因此下一个字符 b 从字节索引 4 开始。
总结:
使用下标
s[i]得到的是字节。使用
range遍历字符串时,value 是rune,index 是该 rune 在字符串中的字节偏移量,而不是字符序号。
字符串截取
理解字节和 rune 的区别后,下面的问题就比较明确:
s := "中文字符"
fmt.Println(s[0:2]) // 得到无效的 UTF-8 字节序列
字符串切片 s[0:2] 是按字节索引进行的。中 的 UTF-8 编码占 3 个字节,而这里仅截取了前 2 个字节,因此得到的结果不再是完整、有效的 UTF-8 编码。
如果需要按 Unicode 码点截取,可以先转换为 []rune:
runes := []rune(s)
fmt.Println(string(runes[0:2])) // 输出 "中文"
因此,当字符串中可能包含中文、日文、emoji 等多字节编码内容,并且需要按 Unicode 码点进行截取时,不应直接使用字节索引,而应先转换为 []rune。
不过,[]rune 只能保证不会截断单个 Unicode 码点。对于由多个码点组成的 emoji 或组合字符,如果需要按照用户视觉上的完整字符截取,还需要进一步进行 Unicode 字素簇处理。
类型转换
明确了各自的语义后,类型转换的区别主要在于按“字节”还是按“Unicode 码点”处理:
s := "你好"
// 按字节拆分和拼接
bs := []byte(s) // [228 189 160 229 165 189]
s1 := string(bs) // "你好"
// 按 Unicode 码点拆分和拼接
rs := []rune(s) // [20320 22909]
s2 := string(rs) // "你好"
另一个需要注意的细节是,将整数直接转换为 string 时,Go 会将这个整数视为 Unicode 码点:
fmt.Println(string(rune(65))) // "A",因为 65 是 'A' 的 Unicode 码点
它不会得到字符串 "65"。如果需要将整数转换为对应的十进制文本,应使用 strconv.Itoa:
strconv.Itoa(65) // "65"
选择
在网络传输、文件读写、二进制协议解析等以原始字节为处理对象的场景中,通常使用 []byte。这些场景关注的是字节流,而不是 Unicode 文本。
当需要按 Unicode 码点处理文本,例如统计码点数量、按码点截取或反转文本时,可以使用 []rune,避免将 UTF-8 编码中的单个码点从中间截断。
如果只是存储、传递或拼接文本,通常直接使用 string。字符串不可变,是 Go 中表示文本最常用的类型。
如果需要频繁构造或拼接字符串,则不适合反复使用 + 生成新的字符串。此时可以根据具体场景使用 []byte、strings.Builder 或 bytes.Buffer,以减少不必要的内存分配。
评论