Go 的 string 是只读结构体,含 data 指针和 len 字段,赋值仅拷贝16字节;不可修改、子串复用底层数组、转 []byte 必深拷贝;拼接推荐 strings.Builder;遍历中文需 []rune;不可变性是内存保护机制。

Go 的 string 不是数组,而是一个只读结构体
Go 的 string 类型在内存里就是两个字段:data(uintptr 类型指针)和 len(int)。它不存字符本身,只存“在哪”和“多长”。你写 s := "hello",编译器会把 "hello" 字面量塞进只读数据段,s 的 data 指向那里,len 是 5。
- 不能通过
s[0] = 'H'修改 —— 编译直接报错:cannot assign to s[0] - 哪怕用
unsafe.StringHeader强转,再改底层内存,运行时大概率 panic 或崩溃,因为那块内存是 mmap 的PROT_READ页 -
string赋值(比如s2 := s1)只拷贝这两个字段(16 字节),不拷贝底层数组,所以非常快
为什么 []byte(s) 会新建内存,而不是共享?
因为 string 底层指向的是只读内存,而 []byte 是可写的切片。Go 必须做一次深拷贝,否则你就可能意外修改字符串字面量 —— 这会破坏整个程序的稳定性。
- 示例:
s := "abc"; b := []byte(s); b[0] = 'x'→s还是"abc",没变;b是独立副本 - 如果想避免拷贝,又需要读写,就别从
string开始,直接用[]byte构建数据 - 注意:子串操作如
s[1:3]复用原底层数组,可能导致大字符串长期驻留内存(阻碍 GC),这是真实线上问题
拼接字符串时,+、strings.Builder、bytes.Buffer 怎么选?
拼接不是语法糖,它直接受底层结构制约:每次拼接都得 malloc 新空间、memcpy 旧内容。选法取决于拼接次数和是否已知长度。
-
+:适合 2~3 次固定拼接,比如"HTTP/" + version;超过 5 次就明显拖慢,编译器不会优化成 Builder -
strings.Builder:推荐默认选择;builder.Grow(n)可预分配,避免多次扩容;builder.String()返回 string 时不拷贝(内部复用底层数组) -
bytes.Buffer:兼容老代码或需要同时写入二进制/文本时用;但Buffer.String()每次都拷贝,不如 Builder 高效
遍历中文字符串时,为什么 s[i] 不是字符?
因为 string 底层是 UTF-8 字节数组,而中文一个字符占 3 个字节。s[0] 拿到的是第一个字节(比如 228),不是“中”字本身 —— 这是 Go 和 UTF-8 编码共同决定的,不是 bug。
立即学习“go语言免费学习笔记(深入)”;
- 要按字符遍历,必须转
[]rune:for _, r := range []rune(s) { ... } - 但注意:
[]rune(s)会完整拷贝并解码整个字符串,对长文本有性能开销 - 如果只是查找、切分、匹配,优先用
strings包函数(它们内部做字节级优化,不强制转 rune)
真正容易被忽略的点是:字符串不可变性不只是“语法限制”,它是运行时内存保护机制的一部分。一旦绕过类型系统去碰 StringHeader 或 unsafe,你就等于主动跳过这层保护 —— 在生产环境里,这不是“能不能”,而是“值不值得冒这个险”。


















