
go 的字符串内容不可变(即底层字节数组不可修改),但字符串变量是可变的——其内部指针和长度字段可被更新,因此变量地址不变,而实际指向的数据可能已更换。
go 的字符串内容不可变(即底层字节数组不可修改),但字符串变量是可变的——其内部指针和长度字段可被更新,因此变量地址不变,而实际指向的数据可能已更换。
在 Go 中,“字符串不可变”(immutable strings)是一个常被误解的概念。它并非指字符串变量不能被重新赋值,而是指字符串所表示的底层字节序列一旦创建便不可原地修改。换句话说:你无法通过索引(如 s[0] = 'x')更改字符串中的某个字符,也不能用 unsafe 以外的方式覆写其底层内存;但你可以随时将一个字符串变量赋值为另一个字符串字面量或拼接结果——此时变量内部的指针和长度字段会被更新,指向新的底层数组。
这正是示例中 &c1.name 地址始终不变的根本原因:c1.name 是结构体中的一个字符串变量,其内存位置(即结构体内偏移地址)是固定的。当执行 z.name = "Rocky" 时,Go 并未修改 "Todd" 所在的内存,而是将 z.name 的内部 StringHeader(含 Data *byte 和 Len int)更新为指向新字符串 "Rocky" 的只读数据段。变量本身的栈地址(&c1.name)自然保持不变。
可通过 reflect.StringHeader 直观观察这一过程:
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
var s string = "abc"
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Printf("s=%q, &s=%p, header={Data:%#x, Len:%d}\n", s, &s, hdr.Data, hdr.Len)
// 输出类似:s="abc", &s=0xc000014080, header={Data:0x10a9b38, Len:3}
s = "cde" // 重新赋值 → 指向新底层数组
hdr = (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Printf("s=%q, &s=%p, header={Data:%#x, Len:%d}\n", s, &s, hdr.Data, hdr.Len)
// &s 不变,但 Data 已不同
s = s[1:] // 切片 → 共享原底层数组,Data 偏移变化
hdr = (*reflect.StringHeader)(unsafe.Pointer(&s))
fmt.Printf("s=%q, &s=%p, header={Data:%#x, Len:%d}\n", s, &s, hdr.Data, hdr.Len)
}⚠️ 注意事项:
- 永远不要用 unsafe 修改字符串底层内存,这会破坏内存安全与 GC 正确性;
- 字符串比较、哈希、切片等操作依赖其不可变性,编译器可据此做深度优化(如字符串字面量去重、共享底层数组);
- 若需频繁修改文本,请使用 strings.Builder 或 []byte,而非反复拼接字符串(避免重复分配)。
总结:Go 的字符串“不可变”是值语义层面的安全保证,而非变量绑定的锁定。理解 string 类型的双字段结构(Data + Len)及其与变量地址的分离,是掌握 Go 内存模型的关键一步。


















