Go 字符串

string 是不可变字节序列;len 计字节,range 产 rune,与 []byte 转换有成本;UTF-8 与码点、字素簇不可混为一谈。

#type / concept #status / growing #tech / dev #resource / go

[!info] 关联笔记

Go 字符串

这个概念为什么会出现

文本处理几乎无处不在:日志、路径、协议、UI 文案、配置键。若语言把“字符”定义含糊,国际化与切片会一起爆炸。

Go 的选择非常明确:

  • string = 不可变的字节序列
  • 源码与惯例编码 = UTF-8
  • byte = uint8
  • rune = int32,表示 Unicode 码点

它不把 string 伪装成“可索引的字符数组对象”,而是逼你分清:字节、码点、字素簇三层。

[!abstract] 一句话理解 string 不可变,len(s) 是字节数;用下标得到 byte,用 range 得到 rune(码点)与其起始字节下标;string[]byte 转换在语义上是复制,构建大量文本优先 strings.Builder

最小可运行示例

先把示例放进业务场景,再看代码:

场景:用户昵称校验与展示截断

注册接口收到昵称 "Hello, 世界"。产品规则:

  1. 统计“字符数”做长度限制(不能只看 len 字节)
  2. 审计日志要按字符遍历
  3. 错误地按字节截断预览会切坏汉字
  4. 脱敏时改的是拷贝,不能动到原字符串
  5. 拼欢迎语时用 Builder,避免循环 + 分配
package main

import (
	"fmt"
	"strings"
	"unicode/utf8"
)

func main() {
	// 用户提交的昵称:ASCII + 中文混排
	nickname := "Hello, 世界"

	// 1) 长度限制:产品说“最多 20 个字”,必须数 rune,不是字节
	fmt.Println("bytes:", len(nickname))                    // 13
	fmt.Println("runes:", utf8.RuneCountInString(nickname)) // 9

	// 2) 按字节切片:下标是字节偏移;“世”占 3 字节,从 7 起
	fmt.Printf("hanzi bytes: %x\n", nickname[7:10]) // e4b896

	// 3) range 字符串:i 是字节下标,r 是解码后的 rune
	// 业务:审计每个字符;注意 i 不是 0,1,2… 连续 +1
	for i, r := range nickname {
		fmt.Printf("at byte %d: %c %U\n", i, r, r)
	}

	// 4) 错误截断:按字节切到 8,可能切断多字节字符 → 非法 UTF-8
	brokenPreview := nickname[:8]
	fmt.Printf("broken=%q valid=%v\n", brokenPreview, utf8.ValidString(brokenPreview))

	// 5) 字符串不可变:转 []byte 得到可改副本;改副本不影响原昵称
	buf := []byte(nickname)
	buf[0] = 'h'
	fmt.Println("original:", nickname, "copy:", string(buf))

	// 6) 拼装欢迎语:多次追加用 Builder
	var b strings.Builder
	b.WriteString("welcome, ")
	b.WriteString(nickname)
	fmt.Println(b.String())
}

建议运行:

go run .

期望输出要点:bytes: 13runes: 9broken 非法、original 仍以 H 开头、欢迎语完整。

结合场景再看四个关注点

  1. len 是字节数,不是“字数”
    中文昵称长度校验必须用 utf8.RuneCountInString 或按 rune 遍历。

  2. range 产 rune,索引是字节偏移
    不要假设下标每次 +1。

  3. 按字节乱切会破坏 UTF-8
    预览截断应先转 []rune 或用安全截断库。

  4. string 不可变;拼接用 Builder
    []byte(s) 是拷贝;热路径少写 s = s + x

核心概念与准确模型

语言中的 string

规范:String types

  • 值是 8 位字节的不可变序列
  • 可包含 任意 字节,不保证一定是合法 UTF-8
  • len(s) → 字节数
  • s[i] → 第 i 个字节(byte),i 越界 panic
  • 字符串常量/比较/切片 s[low:high]字节下标

运行时内部常用类似 (ptr, len) 的表示,但是否共享、是否只读映射属于实现;可移植代码依赖语义而非 header 布局。

不可变

// s[0] = 'x'  // 编译错误
s = s + "!"    // 得到新字符串值

不可变带来:

  • 并发只读共享安全(在无 data race 写入底层的前提下)
  • “修改”必有新值或转到 []byte

byte 与 rune

类型含义典型用途
byte (uint8)一个字节协议、二进制、UTF-8 单元
rune (int32)Unicode 码点文本解码后的字符处理

字符字面量:

var r rune = ''
var b byte = 'A'

字面量三态:引号怎么选

源码里 ''""` 是三种不同类型的字面量,Go 靠引号字符本身区分类型,而不是「只是风格不同」:

写法类型含义适合
'a'rune (int32)单个 Unicode 码点单字符、'\n''\xFF'
"hello\n"string普通字符串,支持转义绝大多数字符串
`raw`string原始字符串,不转义、可换行正则、SQL、多行文本、含大量反斜杠或引号

要点:

  • 'x' 只能包一个码点;'ab' 编译报 illegal rune literal(从 JS/Python 带来单引号字符串习惯的新手最常见报错)。
  • `C:\Users\baker` 里反斜杠不转义,写 Windows 路径、正则、JSON 串不必层层转义。
  • 反引号字符串原样保留换行与引号,适合多行模板。
'a'                 // rune, 值 97
''                // rune, 一个汉字也是单个码点
'\n'               // rune, 换行
'ab'               // 编译错误:illegal rune literal
"ab"               // string, OK
`C:\Users\baker`   // string, 反斜杠未转义

[!warning] 常见误区:从 JS/Python 带来单引号字符串 错误:fmt.Println('line count', n)
正确:fmt.Println("line count", n)(或反引号原始串)。

range 与 for 字节循环

for i, r := range s { /* r 是 rune,i 是该码点起始字节索引 */ }
for i := 0; i < len(s); i++ { /* s[i] 是 byte */ }

非法 UTF-8 序列在 range 时会变成 0xFFFD(替换字符)并前进一字节——解码规则见规范与 unicode/utf8

码点 ≠ 用户感知字符

  • 一个用户看到的字可能由多个码点组成(组合音标、emoji ZWJ 序列等)
  • utf8.RuneCountInString 数码点,不是字素簇
  • 需要字素簇时用 golang.org/x/text/unicode/norm 等扩展库思路,不在本篇展开

string 与 []byte / []rune

b := []byte(s)   // 语义:复制字节
s2 := string(b)  // 语义:复制为不可变字符串
r := []rune(s)   // 解码为码点切片(成本更高)
s3 := string(r)  // 重新编码为 UTF-8

注意:

  1. 编译器可能在证明安全时优化掉拷贝,但这是优化不是许可证:不要用 unsafe 改 string 底层。
  2. 热路径上反复 string([]byte) 会显眼分配;能传 []byte 就保持 []byte
  3. []rune 往返适合“按码点编辑”,不适合当默认存储。

拼接与 Builder

// 少量:+
// 循环大量:
var b strings.Builder
b.Grow(n) // 可选预分配
b.WriteString(s)
_ = b.String()

bytes.Buffer 更偏二进制缓冲;纯字符串累积优先 strings.Builder

strings 与 unicode/utf8 包

  • stringsContains Split Join Trim Replace Map
  • unicode/utf8Valid DecodeRuneInString RuneCountInString
  • strconv:数字与字符串转换

Split 等仍是字节/分隔符语义;按 rune 边界处理要用 utf8 或专门逻辑。

比较与有序

字符串可比较,按字节字典序(不是语言 locale 排序)。作 map key 很常见。

设计动机

  1. 数据就是字节
    网络与文件本就是字节;string 不撒谎。
  2. UTF-8 默认
    与互联网主编码一致,ASCII 兼容。
  3. 不可变简化共享
    只读传递代价低、心智简单。

边界情况与反直觉行为

  1. len("世") == 3
  2. s[0] 对中文首字节不是完整字
  3. 字节切片切断 UTF-8 中间 → 非法字符串
  4. range 的 index 有空洞(多字节)
  5. 空字符串与 nil []byte
    string(nil []byte)""[]byte(nil string) 相关转换注意 nil/empty 差异
  6. 映射文件/超大字符串
    仍受不可变与转换成本约束

常见误区

[!warning] 常见误区:用 len 当“字符数” 错误:校验用户名长度直接 len(s) <= 10 当“10 个字”。
正确:先定义要限制的是字节、码点还是字素;再用对应 API。

[!warning] 常见误区:for i := range 当 rune 循环却用 s[i:] 乱切 错误:假设每个 i 都是等宽字符边界。
正确:使用 range 的 rune,或 utf8.DecodeRuneInString

[!warning] 常见误区:热循环 string + string 错误:每次分配新字符串导致平方级拷贝。
正确:strings.Builder[]byte 累积。

[!warning] 常见误区:用 unsafe 改 string 底层 错误:为了零拷贝直接写 string header 指向的内存。
正确:保持不可变契约;需要改就用 []byte

工程实践

  1. API 边界:文本用 string,二进制用 []byte,别混。
  2. 校验 UTF-8:读入外部数据时 utf8.ValidString
  3. 日志与截断:按字节截断可能切断中文;展示层要按 rune/字素策略。
  4. 比较:鉴权 token 等敏感比较考虑恒定时间(subtle.ConstantTimeCompare),普通业务用 ==
  5. 国际化:不要假设 1 rune = 1 显示宽度。
  6. 性能:减少热路径类型来回转;能借用 []byte 的库函数优先。

可验证实验

  1. 打印 "Go语言"lenRuneCountInString
  2. 对比 for i := range sfor i, r := range s
  3. s[:n] 切在多字节中间,ValidString 为 false。
  4. 循环 +Builder 的 benchmark(见 go-benchmarking)。
  5. 修改 []byte(s) 后原 s 是否变化。

本节总结

  • 本质:不可变字节序列;UTF-8 是惯例与源码编码。
  • 三层:byte / rune(码点) / 字素簇。
  • 关键操作:len、下标、range、转换、Builder。
  • 最易错:字符计数、乱切片、拼接分配、破坏不可变。
  • 下一步:slice 字节缓冲与 io 文本处理。

自测题

概念题

  1. len(s) 数的是什么?
  2. range s 中的 r 是什么?索引 i 的含义?
  3. 为什么 string[]byte 转换可能贵?

代码推理题

s := "a中"
fmt.Println(len(s))
for i, r := range s {
    fmt.Println(i, string(r), r)
}

大致输出结构是什么?

工程思考题

HTTP 头、文件路径、用户评论三种字符串,校验“长度”时分别更可能用字节限制还是码点限制?为什么?

参考答案

展开
  1. 字节数。
  2. r 是码点(rune);i 是该码点第一个字节的下标。
  3. 语义上需要得到独立可变/不可变底层,通常伴随复制与分配(实现优化除外)。
    代码:len 为 1+3=4;先打印 0 a,再打印 1 中(索引跳到 1 而非 2)。
    工程:协议/头/路径常有字节上限(线宽、系统 API);用户可见文案更常谈码点/字素;需按威胁模型与平台约束选择并写清文档。

延伸阅读与资料来源

资料类型支撑
Spec — String types规范不可变字节序列
Spec — For statements / range规范字符串 range
Strings, bytes, runes and characters in Go博客权威概念文
Package strings标准库文本工具
Package unicode/utf8标准库UTF-8 编解码

笔记元信息

  • 建议文件名:go-strings.md
  • 所属阶段:阶段一
  • 本篇状态:已深化
  • 建议下一篇:Go Slice / range 语义
创建于 2026/6/25 更新于 2026/7/28