rune 与 byte
本教程共 80 篇 · 第 13 篇 · 更新于 2026-07-27 · 约 7 分钟阅读
13. rune 与 byte
本节目标:彻底搞懂 rune 和 byte 的区别,理解 Unicode 码点,学会正确处理含中文的字符串。
为什么需要两个类型
先说结论:byte 和 rune 都是整数类型的别名,但用途不同。
type byte = uint8 // 字节,1个字节
type rune = int32 // 字符,4个字节
byte用来表示一个字节(8 位)rune用来表示一个 Unicode 码点(Code Point)
在处理 ASCII 文本时,byte 和 rune 看起来没区别。但一旦涉及中文、表情符号等多字节字符,区别就大了。
Unicode 和 UTF-8
Unicode 码点
Unicode 给世界上每个字符分配了一个唯一的数字编号,叫码点。
| 字符 | 码点 |
|---|---|
| A | U+0041(十进制 65) |
| 中 | U+4E2D(十进制 20013) |
| 😀 | U+1F600(十进制 128512) |
rune 就是用来存这些码点的。它用 int32,能表示 0 到 0x10FFFF 的所有 Unicode 码点。
UTF-8 编码
UTF-8 是一种把 Unicode 码点编码成字节序列的方式:
| 码点范围 | 编码字节数 |
|---|---|
| 0-127 | 1 字节 |
| 128-2047 | 2 字节 |
| 2048-65535 | 3 字节 |
| 65536+ | 4 字节 |
ASCII 字符(0-127)在 UTF-8 中只占 1 字节,和 ASCII 编码完全兼容。中文通常占 3 字节,表情符号占 4 字节。
Go 字符串用 UTF-8 编码存储。一个 byte 存一个字节,一个 rune 存一个完整的码点。
byte 的用法
byte 就是 uint8,表示一个字节。
var b byte = 65
fmt.Println(b) // 65
fmt.Printf("%c\n", b) // A(把 65 当 ASCII 字符输出)
fmt.Printf("%T\n", b) // uint8
字符串索引 s[i] 返回的就是 byte:
s := "Hello"
b := s[0] // byte 类型,值是 72
fmt.Printf("%c\n", b) // H
byte 适合处理纯 ASCII 文本,或者操作二进制数据。
rune 的用法
rune 就是 int32,表示一个 Unicode 码点。
用单引号写的字符字面量,默认类型是 rune:
r := 'A'
fmt.Printf("%T\n", r) // int32
fmt.Println(r) // 65
r2 := '中'
fmt.Println(r2) // 20013
fmt.Printf("%c\n", r2) // 中
字符串与 rune 的转换
string 转 []rune
s := "Hello,世界"
runes := []rune(s)
fmt.Println(len(runes)) // 8(8个字符,不是字节数)
for _, r := range runes {
fmt.Printf("%c ", r) // H e l l o , 世 界
}
转成 []rune 后,每个元素就是一个完整的字符,可以按字符索引访问。
[]rune 转 string
runes := []rune{72, 101, 108, 108, 111}
s := string(runes)
fmt.Println(s) // Hello
string 转 []byte
s := "Hello"
bytes := []byte(s)
fmt.Println(bytes) // [72 101 108 108 111]
[]byte 转 string
bytes := []byte{72, 101, 108, 108, 111}
s := string(bytes)
fmt.Println(s) // Hello
两种遍历方式对比
按字节遍历(for + len)
s := "Hello,世界"
for i := 0; i < len(s); i++ {
fmt.Printf("%c ", s[i])
}
// 输出:H e l l o , ä ¸ ç
中文被拆成了乱码,因为每个中文字符占 3 字节,按字节遍历会拆开。
按字符遍历(for-range)
s := "Hello,世界"
for _, r := range s {
fmt.Printf("%c ", r)
}
// 输出:H e l l o , 世 界
for-range 自动按 UTF-8 解码,每次得到一个完整的 rune。
Note
for-range遍历字符串时,第二个变量是rune类型,不是byte。这是 Go 语言设计上的贴心之处。
实际应用
统计字符数
s := "Hello,世界"
fmt.Println("字节数:", len(s)) // 12
fmt.Println("字符数:", utf8.RuneCountInString(s)) // 8
fmt.Println("字符数:", len([]rune(s))) // 8
按字符索引访问
s := "你好Go"
runes := []rune(s)
fmt.Printf("%c\n", runes[0]) // 你
fmt.Printf("%c\n", runes[3]) // G
直接用 s[0] 只能拿到第一个字节,转成 []rune 后才能按字符索引。
修改字符串中的字符
s := "Hello"
runes := []rune(s)
runes[0] = 'h'
s = string(runes)
fmt.Println(s) // hello
判断字符类型
unicode 包提供了判断字符类型的函数:
import "unicode"
func main() {
r := '中'
fmt.Println(unicode.Is(unicode.Han, r)) // true,是汉字
fmt.Println(unicode.IsLetter(r)) // true,是字母
fmt.Println(unicode.IsDigit('5')) // true,是数字
fmt.Println(unicode.IsSpace(' ')) // true,是空白字符
}
转大小写
import "unicode"
func main() {
r := 'a'
fmt.Printf("%c\n", unicode.ToUpper(r)) // A
fmt.Printf("%c\n", unicode.ToLower('B')) // b
}
byte 和 rune 对比
| 特性 | byte | rune |
|---|---|---|
| 底层类型 | uint8 | int32 |
| 大小 | 1 字节 | 4 字节 |
| 表示 | 一个字节 | 一个 Unicode 码点 |
| 字符串索引返回 | 是 | 否 |
| for-range 返回 | 否 | 是 |
| 适合场景 | 二进制数据、ASCII | 多语言文本 |
字符串常量是 UTF-8
Go 源代码文件本身必须是 UTF-8 编码。你在字符串字面量里写的中文,编译后就是 UTF-8 字节序列:
s := "中文"
// s 的底层是 [228, 184, 173, 230, 150, 135]
这也是 Go 原生支持多语言的原因—从源码到运行时,全链路 UTF-8。
常见问题
单引号和双引号的区别
s := "A" // 字符串,类型是 string
c := 'A' // 字符,类型是 int32(rune)
双引号是字符串,单引号是字符(rune)。这个区别一定要记住。
string(65) 和 string([]byte{65})
fmt.Println(string(65)) // A,把码点 65 转成字符
fmt.Println(string([]byte{65})) // A,把字节序列转成字符串
结果一样,但含义不同。string(65) 是把整数当码点转字符,string([]byte{65}) 是把字节序列转字符串。
for-range 的索引不连续
s := "A中"
for i, r := range s {
fmt.Printf("索引%d: %c\n", i, r)
}
// 索引0: A
// 索引1: 中
“A” 占 1 字节,所以第二个字符的索引是 1。“中” 占 3 字节,如果后面还有字符,索引会跳到 4。
学完这节你能做什么
- 分清 byte 和 rune 的用途
- 理解 Unicode 码点和 UTF-8 编码的关系
- 正确遍历包含中文的字符串
- 按字符索引访问和修改字符串
- 用 unicode 包判断字符类型
下一节讲类型转换和类型推断。