5.2 字符串与零拷贝转换
字符串与切片同源,运行时里都是「头部 + 别处的内存」(布局见 5.1.1),
但它只读、不可变,自成一类。不可变换来共享与免拷贝的好处,也带来一个直接代价:string 与
[]byte 互转默认要拷贝一份字节。本节剖开这份拷贝,看编译器在哪些场景下能把它省掉,再到 Go 1.20
给出的手动零拷贝工具,以及由此而来的安全契约。
5.2.1 字符串与 []byte 的转换
字符串不可变带来一个直接代价:string 与 []byte 互转默认要拷贝一份字节。原因正是不可变,
[]byte 可写,若让它直接指向某个字符串的底层字节,改 []byte 就等于改了那个「不可变」的字符串,
破坏了一切共享假设。所以运行时老老实实 memmove 一份:
| |
这份拷贝在热路径上可能可观。好在编译器认得几类「转换后字节立刻被读、不可能被改」的模式,把拷贝
省掉。最常见的两个是用 []byte 临时当 map 键查询,与对 []byte(s) 直接 range:
| |
要在自己代码里手动零拷贝转换,Go 1.20 给了正式工具:unsafe.String(*byte, len) 把一段字节当
字符串看,unsafe.StringData(string) *byte 取字符串底层指针,unsafe.Slice / unsafe.SliceData
是切片侧的对应物。它们取代了过去靠 reflect.StringHeader / reflect.SliceHeader 手工拼头部
的脆弱写法(那种写法在有 GC 移动与字段对齐变化时并不可靠)。代价是你要自己担保转换之后那段
字节不再被修改,否则就把不可变契约捅破了:
| |
延伸阅读的文献
- The Go Authors. runtime/string.go:
slicebytetostring/stringStruct(字符串布局与转换). https://github.com/golang/go/blob/master/src/runtime/string.go - The Go Authors. Go 1.20 Release Notes(
unsafe.String/unsafe.StringData/unsafe.SliceData). https://go.dev/doc/go1.20 - Rob Pike. Strings, bytes, runes and characters in Go. The Go Blog, 2013. https://go.dev/blog/strings