1032 字
约 3 分钟
6
S09-J02 · 字符串与 UTF-8:byte、rune 与 O(n) 陷阱
适用读者:处理中文/emoji 字符串时遇到过乱码或截断错误的人。
前置:S09-B02。
实测环境:Go 1.22.2(2026-11-05)。
覆盖旧文:—(E 驱动;B02 §5 的深化)
覆盖扩展:E-S09-002 深化(UTF-8 编码规则)。
上一篇:S09-J01 · 切片底层:cap 增长、共享与内存泄漏
下一篇:S09-J03 · 闭包与捕获的版本差异。
1. 一句话结论
Go 的字符串是"不可变的字节序列",UTF-8 是它的默认编码但不是唯一解释方式。所有"乱码/切坏"问题都源于"用字节下标处理字符"。
2. 实测:按字节切会切坏中文
s := "中文abc"
cut := s[:4]
fmt.Printf("s[:4] 字节=% x 合法UTF8=%v\n", []byte(cut), utf8.ValidString(cut))
实测输出:
s[:4] 字节=e4 b8 ad e6 合法UTF8=false
读法:
| 字节 | 含义 |
|---|---|
e4 b8 ad |
"中"(3 字节,完整) |
e6 |
"文"的首字节(3 字节中的第 1 个)→ 截断 |
正确切一个中文字:
safe := s[:len("中")] // len("中") = 3
fmt.Printf("%q 合法=%v\n", safe, utf8.ValidString(safe))
// 输出:"中" 合法=true
实测汇总:
J02) rune 个数: 5 字节: 9
3. UTF-8 编码规则(为什么中文 3 字节)
| 码点范围 | 字节数 | 首字节 | 后续字节 |
|---|---|---|---|
| U+0000–U+007F | 1 | 0xxxxxxx |
— |
| U+0080–U+07FF | 2 | 110xxxxx |
10xxxxxx |
| U+0800–U+FFFF(含中文) | 3 | 1110xxxx |
10xxxxxx ×2 |
| U+10000–U+10FFFF(emoji) | 4 | 11110xxx |
10xxxxxx ×3 |
0xE4 = 11100100 → 匹配 1110xxxx → 说明这里开始一个 3 字节字符。这就是"切在第 4 字节会坏"的原因。
4. 遍历的两种方式与代价
| 方式 | 写法 | 复杂度 | 结果 |
|---|---|---|---|
| 按字节 | for i := 0; i < len(s); i++ { s[i] } |
O(n) | 每个字节一个 byte |
| 按 rune | for i, r := range s { } |
O(n) | 每个字符一个 rune,i 是起始字节偏移 |
| 转切片遍历 | for _, r := range []rune(s) { } |
O(n) + 分配 | 先生成 rune 数组(额外内存) |
性能提示:[]rune(s) 会分配新数组;只需遍历时用 range 即可,别先转换。
"O(n) 陷阱":len([]rune(s)) 是 O(n) 且分配;如果只需要判断"字符数是否 ≤ N",用 utf8.RuneCountInString 仍是 O(n),但可配合 utf8.RuneCount 逐个边界检查提前退出。
5. 安全截断的三种写法
// ① 按 rune 数截断
func truncateRunes(s string, n int) string {
r := []rune(s)
if len(r) <= n { return s }
return string(r[:n])
}
// ② 按字节上限截断(保证不切坏字符)
func truncateBytes(s string, max int) string {
if len(s) <= max { return s }
for max > 0 && !utf8.RuneStart(s[max]) { max-- } // 回退到字符起始
return s[:max]
}
// ③ 用 strings 包
// strings.ToValidUTF8(s, "�") 把非法字节替换掉
| 需求 | 用哪个 |
|---|---|
| "最多 N 个字"(展示) | ① |
| "最多 N 字节"(存储/协议限制) | ② |
| 清洗脏数据 | ③ |
6. 五个误区
| 误区 | 真相 |
|---|---|
"len(s) 是字符数" |
是字节数 |
"s[i] 拿到字符" |
拿到第 i 个 byte |
"s[:n] 是安全截断" |
可能切坏 UTF-8 |
"[]rune(s) 很便宜" |
O(n) 并分配新数组 |
| "Go 字符串是 UTF-16/UTF-32" | 是 UTF-8 字节序列 |
7. 自检
"中文abc"的字节数与 rune 数分别是多少?s[:4]为什么非法?- 如何安全地"最多保留 N 个字符"?
range字符串时的i是什么?[]rune(s)的代价是什么?什么时候不必用?
8. 下一篇
S09-J03 · 闭包与捕获:从 1.21 到 1.22 的语义变更
编码问题解决后,最后一篇进阶层处理"版本升级导致的行为变化":闭包捕获语义。
本篇属于系列《S09 · Go 语言》。扩展点 E-S09-002 深化。UTF-8 截断与合法性问题为 Go 1.22.2 实测。
S09-J02 · 字符串与 UTF-8:byte、rune 与 O(n) 陷阱
https://www.clxhxhhr.top/posts/5307/ 评论
0 条
还没有评论,先写一条吧。