string 为什么不可变?string 与 []byte 相互转换的代价是什么?

结论先行

string 在语言层被设计为只读的字节序列:下标访问、切片、比较都不会改写其内容,也没有原地修改的 API;正因不可变,它可以安全地作为 map 的 key、被多个 goroutine 共享、并被编译器放心地做常量折叠等优化。而 string 与 []byte 互转大多伴随 O(n) 的拷贝与堆分配,只有少数场景编译器会走免拷贝的快速路径。

要点

  • 底层视图:string 本质是“指向字节数组的指针 + 长度”两个字段,内容无法通过正常途径修改。
  • 不可变带来的收益:map key 哈希稳定、跨 goroutine 只读共享零风险、编译期可做常量折叠与子串复用。
  • []byte(string):在堆上分配一份新字节数组并整体拷贝,代价 O(n),结果可变且与原 string 无关。
  • string([]byte):需要复制出只读的字节区,代价同样是 O(n) 拷贝加一次分配。
  • 查找特例:m[string(b)] 这类“转换结果只用于 map 查找、不被保留”的场景走 faststr 快速路径,一般不会为查找复制字符串(编译器/运行时的针对性优化,不构成通用承诺)。
  • 零拷贝有代价:用 unsafe 把 []byte 强转成 string 可免拷贝,但之后修改 b 会破坏 string 的只读假设,可能导致 map 查找错乱等未定义行为,生产代码慎用。
  • 常见优化点:高频拼接用 strings.Builder(内部 []byte),避免每次 string([]byte) 都复制。

示例

b := []byte("hello")
s := string(b)   // 拷贝:s 与 b 已无关联
b[0] = 'H'       // s 仍是 "hello"

m := map[string]int{}
m[string(b)] = 1 // 查找场景:faststr 路径,一般免拷贝
s2 := string(b)  // 普通转换:仍会拷贝

// 零拷贝但危险:unsafe.String 直接复用 b 的数组,
// 之后任何写 b 的操作都会让 s 的内容“变脏”,务必避免。

常见追问/记忆点

  • 追问:为什么需要 []byte 参与运算?答:拼接、加解密、网络收发多基于可变字节切片,string 负责表达只读文本。
  • 追问:string 的 == 比较是 O(n) 吗?答:最坏按字节比较是 O(n),但编译器对字面量等场景有预比较优化。
  • 追问:想零拷贝转换该怎么办?答:unsafe.String/unsafe.SliceData,前提是严格保证原数据不再被修改。
  • 记忆:转换即拷贝(个别查找场景除外);想复用必须 unsafe,但别破坏不可变承诺。
笔记加载中…