1032 字
约 3 分钟
6
S09-J02 · 字符串与 UTF-8:byte、rune 与 O(n) 陷阱
2024-12-31
2026-11-07

适用读者:处理中文/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. 自检

  1. "中文abc" 的字节数与 rune 数分别是多少?
  2. s[:4] 为什么非法?
  3. 如何安全地"最多保留 N 个字符"?
  4. range 字符串时的 i 是什么?
  5. []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/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。