本文golang源码为1.18版本
标准转换
使用标准转换是最常见的选择
package main
func main() {
s := "Hello, world!"
// string转byte数组
b := []byte(s)
// byte数组转string
s2 := string(b)
}
该转换语句会被go编译器翻译为runtime层的方法调用,其中[]byte
到string
的转换对应/src/runtime/string.go:81
处的slicebytetostring
函数;而string
到[]byte
的转换对应的则是/src/runtime/string.go:172
处的stringtoslicebyte
函数。
先来看看比较简单的stringtoslicebyte
函数,tmpBuf
类型是个大小为32的byte
数组。
// The constant is known to the compiler.
// There is no fundamental theory behind this number. 🤣
const tmpStringBufSize = 32
type tmpBuf [tmpStringBufSize]byte
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
var b []byte
if buf != nil && len(s) <= len(buf) {
*buf = tmpBuf{}
b = buf[:len(s)]
} else {
// 如果没有缓冲区或缓冲区大小不足,需要申请内存
b = rawbyteslice(len(s))
}
// 复制数据
copy(b, s)
return b
}
func rawbyteslice(size int) (b []byte) {
// 容量计算,考虑内存对齐,寻找大小最匹配的内存块
cap := roundupsize(uintptr(size))
// 使用mallocgc申请对应大小的内存
p := mallocgc(cap, nil, false)
// 如果要申请的size和最终计算得到的cap大小不一致,cap只会比size更大,清理掉多余的内存
if cap != uintptr(size) {
memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size))
}
// 将b指向这片申请好的内存。该slice不为空,故外部使用copy进行覆盖而不是append
*(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)}
return
}
当需要转换的字符串长度小于32时,只会进行内存复制,而大于32的话,除了复制操作,还需要分配内存。
再看看slicebytetostring
函数,
// ptr指向slice的第一个元素,n是slice的长度
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) {
if n == 0 {
return ""
}
...
// 如果只有单个byte,返回值字符串中的数据指针str直接指向预分配好的一块静态内存区
if n == 1 {
// staticuint64s是一个uint64数组,里面一个个值都对应着单个byte的int8值,提供此种情况下的str不可修改的指向
p := unsafe.Pointer(&staticuint64s[*ptr])
if goarch.BigEndian {
p = add(p, 7)
}
// 填充返回数据
stringStructOf(&str).str = p
stringStructOf(&str).len = 1
return
}
var p unsafe.Pointer
if buf != nil && n <= len(buf) {
p = unsafe.Pointer(buf)
} else {
// 缓冲区不存在或者不够时,分配内存
p = mallocgc(uintptr(n), nil, false)
}
stringStructOf(&str).str = p
stringStructOf(&str).len = n
// 使用memmove复制数据
memmove(p, unsafe.Pointer(ptr), uintptr(n))
return
}
关于staticuint64s
的一段测试代码:
package main
import "unsafe"
var staticuint64s = [...]uint64{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f,
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37,
0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f,
0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57,
0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f,
0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f,
0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,
0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f,
0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,
0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf,
0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7,
0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef,
0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,
0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff,
}
type stringStruct struct {
str unsafe.Pointer
len int
}
func stringStructOf(sp *string) *stringStruct {
return (*stringStruct)(unsafe.Pointer(sp))
}
func main() {
var s string
stringStructOf(&s).str = unsafe.Pointer(&staticuint64s[80])
stringStructOf(&s).len = 1
println(s)
}
使用标准转换是最简单也是最安全的,但从上面的源码可以看到,string
和[]byte
的两个相互转换,都可能出现新的内存分配,并且一定要进行内存的复制。这样的转换在性能敏感的场景下无法满足要求,还好我们还有其他的转换黑魔法😈。
零拷贝转换
编译器转换
其实在上文提到的两个函数旁边,还有一个特殊的函数——slicebytetostringtmp(src/runtime/string.go:154)
// slicebytetostringtmp returns a "string" referring to the actual []byte bytes.
//
// Callers need to ensure that the returned string will not be used after
// the calling goroutine modifies the original slice or synchronizes with
// another goroutine.
//
// The function is only called when instrumenting
// and otherwise intrinsified by the compiler.
//
// Some internal compiler optimizations use this function.
// - Used for m[T1{... Tn{..., string(k), ...} ...}] and m[string(k)]
// where k is []byte, T1 to Tn is a nesting of struct and array literals.
// - Used for "<"+string(b)+">" concatenation where b is []byte.
// - Used for string(b)=="foo" comparison where b is []byte.
func slicebytetostringtmp(ptr *byte, n int) (str string) {
if raceenabled && n > 0 {
racereadrangepc(unsafe.Pointer(ptr),
uintptr(n),
getcallerpc(),
abi.FuncPCABIInternal(slicebytetostringtmp))
}
if msanenabled && n > 0 {
msanread(unsafe.Pointer(ptr), uintptr(n))
}
if asanenabled && n > 0 {
asanread(unsafe.Pointer(ptr), uintptr(n))
}
stringStructOf(&str).str = unsafe.Pointer(ptr)
stringStructOf(&str).len = n
return
}
抛开无需关心的代码,核心代码只有两句:
stringStructOf(&str).str = unsafe.Pointer(ptr)
stringStructOf(&str).len = n
返回字符串的数据指针str直接指向了[]byte
的首地址,而长度直接取[]byte
长度。能够如此转换的根本原因是string的底层类型和slice的底层类型内存分布相似,所以可以直接通过修改指针的方式进行转换。
// src/runtime/string.go:238
type stringStruct struct {
str unsafe.Pointer
len int
}
// src/runtime/slice.go:15
type slice struct {
array unsafe.Pointer
len int
cap int
}
可见,两个结构体的前两个字段内存分布是完全一致的,并且string
的底层数据类型和[]byte
一致,所以unsafe.Pointer
对应的实际指针都是*byte
。这样的转换实际上就是指针的转换,不需要进行内存的分配和复制,所以效率非常高。
但是使用这样的代码是有风险的,上文中的代码我特意把注释也贴出来了,注释里第二段明确写到,调用者需要保证,在修改或与其他g同步了原[]byte
后,返回的string
不会再被使用。这是因为你强行创造的这个string
所对应的底层数据并不安全,创造出该string后,你可以直接对原[]byte
进行修改,这违背了go语言字符串不可修改的原则,将会产生无法捕获的错误。
所以这个方法go没有暴露给开发者,仅在源码内部可使用,供编译器在某些适用场景优化性能。
以上两种转换的性能对比:
package main
import (
"testing"
)
const LEN = 33
func getBytes() []byte {
var b []byte
for i := 0; i < LEN; i++ {
b = append(b, ' ')
}
return b
}
func BenchmarkByte2StringOrigin(b *testing.B) {
bytes := getBytes()
for i := 0; i < b.N; i++ {
s := string(bytes)
_ = s
}
}
func BenchmarkByte2StringTmp(b *testing.B) {
bytes := getBytes()
for i := 0; i < b.N; i++ {
s := slicebytetostringtmp(bytes)
_ = s
}
}
// 依赖代码
type stringStruct struct {
str unsafe.Pointer
len int
}
func stringStructOf(sp *string) *stringStruct {
return (*stringStruct)(unsafe.Pointer(sp))
}
func slicebytetostringtmp(b []byte) (str string) {
stringStructOf(&str).str = unsafe.Pointer(&b[0])
stringStructOf(&str).len = len(b)
return
}
和之前源码看到的一致,对于标准转换,长度32是一个分水岭,长度大于32时,就会出现内存分配,同时操作耗时也显著增加。并且随着slice
长度的增加,和指针转换的性能差距会越来越大。
自己实现——unsafe.Pointer
虽然slicebytetostringtmp
我们无法使用,但是通过使用unsafe
包,我们可以自行构造类似的指针转换。
先来一个最简单的版本:
func String2bytesEasy(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s))
}
func Bytes2StringEasy(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
直接使用unsafe.Pointer
作为中间类型进行强转,非常快速,但是会有一个小问题,String2bytesEasy
函数中,返回的slice
中cap
字段是没有赋值的,因为string
类型只有两个字段,而slice
有三个,未赋值的cap
字段的值是随机的,取决于那一块内存的值。
优化版本:
// src/reflect/value.go:2670
type StringHeader struct {
Data uintptr
Len int
}
// src/reflect/value.go:2681
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
func String2bytes(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
return *(*[]byte)(unsafe.Pointer(&bh))
}
这个版本用到了reflect
包中的两个类型StringHeader
和SliceHeader
,它们分别是string
和slice
的runtime
层形态。通过这样的转换后,我们得以直接操作它们的字段。
通过全部的三个字段赋值,最终得到的是一个完整的slice
。
继续深挖,这里的关键是我们创建的中间变量bh
,特殊点在于它的类型reflect.SliceHeader
。如果你打开过源码的unsafe
包,在Point
类型的开头有一大堆注释(以下仅部分):
// (6) Conversion of a reflect.SliceHeader or reflect.StringHeader Data field to or from Pointer.
//
// As in the previous case, the reflect data structures SliceHeader and StringHeader
// declare the field Data as a uintptr to keep callers from changing the result to
// an arbitrary type without first importing "unsafe". However, this means that
// SliceHeader and StringHeader are only valid when interpreting the content
// of an actual slice or string value.
//
// var s string
// hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
// hdr.Data = uintptr(unsafe.Pointer(p)) // case 6 (this case)
// hdr.Len = n
//
// In this usage hdr.Data is really an alternate way to refer to the underlying
// pointer in the string header, not a uintptr variable itself.
//
// In general, reflect.SliceHeader and reflect.StringHeader should be used
// only as *reflect.SliceHeader and *reflect.StringHeader pointing at actual
// slices or strings, never as plain structs.
// A program should not declare or allocate variables of these struct types.
//
// // INVALID: a directly-declared header will not hold Data as a reference.
// var hdr reflect.StringHeader
// hdr.Data = uintptr(unsafe.Pointer(p))
// hdr.Len = n
// s := *(*string)(unsafe.Pointer(&hdr)) // p possibly already lost
//
重点就是这一句——INVALID: a directly-declared header will not hold Data as a reference.
翻译过来就是,一个直接声明的header不会将Data作为引用。
解释一下,Header的Data字段类型是uintptr
而不是unsafe.Pointer
,这两个类型的其中一个差别就是,go的gc不会记录uintptr
的引用,这一点在SliceHeader
和StringHeader
的定义处也有注释进行了说明,它们俩的定义上方都有这么一段:
// Moreover, the Data field is not sufficient to guarantee the data
// it references will not be garbage collected, so programs must keep
// a separate, correctly typed pointer to the underlying data.
这意味着gc在清理内存时并不知道还存在一个Header的Data字段指向了这块内存,就可能会导致在转换的过程中,底层数据已经被gc给清理。
对应上面的例子中:
func String2bytes(s string) []byte {
stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh := reflect.SliceHeader{
Data: stringHeader.Data,
Len: stringHeader.Len,
Cap: stringHeader.Len,
}
// 在执行这一步之前,stringHeader已经没有了用处,而bh没有产生引用,此时gc认为是可以进行回收的
return *(*[]byte)(unsafe.Pointer(&bh))
}
避免这个问题的方法也很简单,避开directly-declared
就可以了,注释中也给了一个正确例子:
var s string
hdr := (*reflect.StringHeader)(unsafe.Pointer(&s)) // case 1
hdr.Data = uintptr(unsafe.Pointer(p)) // case 6 (this case)
hdr.Len = n
这里先创建了临时变量s,再基于s进行转换得到的header指针,所以这里并不存在一个实际的header,对hdr的赋值操作其实是在直接操作字符串s,所以也就没有了gc追踪不到的问题了。
贴一个优化代码示例:
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
func S2b(s string) (b []byte) {
bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
bh.Data = sh.Data
bh.Len = sh.Len
bh.Cap = sh.Len
return b
}
补充
关于这个问题,我还找到一个go源码库的issue:Feature: provide no-copy conversion from []byte to string,虽然issue的发起者是想问为什么没有官方提供的转换函数,但是他提供的代码有着和我所给的反例代码一样的问题,并且被其他用户指出来了,并且这个问题得到了go的组织成员的肯定:
有趣的是,go101(应该有人认识吧)还在下面提出即使避开了header的直接定义,还是需要使用runtime.KeepAlive
来保证gc不会回收数据,这就与我本文观点冲突了🤣。
不过好在这条在下面被反对了👀
至于为什么不把reflect包中header的Data字段改为unsafe.Pointer来解决这个问题,也有了解释:
这里面提到的另外的issue:
unsafe: add Slice(ptr *T, len anyIntegerType) []T
proposal: spec: disallow T<->uintptr conversion for type T unsafe.Pointer
另外,我觉得该issue的发起人说的话挺有道理的,这段转换的代码藏了很深的坑,没有对源码有一定了解的人完全无法发现,而官方又没有提供转换的最佳实践,以致于网络上出现了各种自行实现的版本,而其中许多的实现都是存在问题的。
幸好,这个问题在Go2迎来了转机。
展望未来,Go1.20
在go1.20rc中的这个提交,为slice
和string
的转换提供了新的选择,该提交在unsafe包中新增了四个函数https://github.com/golang/go/blob/release-branch.go1.20/src/unsafe/unsafe.go:
// The function Slice returns a slice whose underlying array starts at ptr
// and whose length and capacity are len.
// Slice(ptr, len) is equivalent to
//
// (*[len]ArbitraryType)(unsafe.Pointer(ptr))[:]
//
// except that, as a special case, if ptr is nil and len is zero,
// Slice returns nil.
//
// The len argument must be of integer type or an untyped constant.
// A constant len argument must be non-negative and representable by a value of type int;
// if it is an untyped constant it is given type int.
// At run time, if len is negative, or if ptr is nil and len is not zero,
// a run-time panic occurs.
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType
// SliceData returns a pointer to the underlying array of the argument
// slice.
// - If cap(slice) > 0, SliceData returns &slice[:1][0].
// - If slice == nil, SliceData returns nil.
// - Otherwise, SliceData returns a non-nil pointer to an
// unspecified memory address.
func SliceData(slice []ArbitraryType) *ArbitraryType
// String returns a string value whose underlying bytes
// start at ptr and whose length is len.
//
// The len argument must be of integer type or an untyped constant.
// A constant len argument must be non-negative and representable by a value of type int;
// if it is an untyped constant it is given type int.
// At run time, if len is negative, or if ptr is nil and len is not zero,
// a run-time panic occurs.
//
// Since Go strings are immutable, the bytes passed to String
// must not be modified afterwards.
func String(ptr *byte, len IntegerType) string
// StringData returns a pointer to the underlying bytes of str.
// For an empty string the return value is unspecified, and may be nil.
//
// Since Go strings are immutable, the bytes returned by StringData
// must not be modified.
func StringData(str string) *byte
由于unsafe包的特殊性,其函数的具体实现是通过汇编等手段实现的,所以你在这里只能看到函数签名。
简单介绍下四个函数的作用:
- Slice:返回一个slice,其底层数组的起始地址即为参数ptr所指向的地址,slice的len和cap都为参数len。
- SliceData: 返回一个指向该slice底层数组的指针。
- String:返回一个string,其底层的byte数组的起始地址即为参数ptr所指向的地址,长度为参数len。
- StringData: 返回参数str底层byte数组的指针。
这四个函数提供的功能非常基础,不过组合起来就能实现我们想要的字符串转换功能了,如果不想安装go1.20rc,你可以在这里去试用这四个函数:
package main
import (
"fmt"
"unsafe"
)
func main() {
str := "hello world!"
bt := unsafe.Slice(unsafe.StringData(str), len(str))
fmt.Println(bt)
}
有了这个写法,reflect的headers就可以弃用了。
附上三种转换的benchmark(IDE可能会报错,但是没事,go1.20可以执行):
package main
import (
"testing"
"unsafe"
)
const LEN = 33
func getBytes() []byte {
var b []byte
for i := 0; i < LEN; i++ {
b = append(b, ' ')
}
return b
}
func getStr() string {
return string(getBytes())
}
func BenchmarkString2Slice(b *testing.B) {
str := getStr()
for i := 0; i < b.N; i++ {
bt := []byte(str)
_ = bt
}
}
func BenchmarkString2SliceReflect(b *testing.B) {
str := getStr()
for i := 0; i < b.N; i++ {
bt := *(*[]byte)(unsafe.Pointer(&str))
_ = bt
}
}
func BenchmarkString2SliceUnsafe(b *testing.B) {
str := getStr()
for i := 0; i < b.N; i++ {
bt := unsafe.Slice(unsafe.StringData(str), len(str))
_ = bt
}
}
func BenchmarkSlice2String(b *testing.B) {
bytes := getBytes()
for i := 0; i < b.N; i++ {
ss := string(bytes)
_ = ss
}
}
func BenchmarkSlice2StringReflect(b *testing.B) {
bytes := getBytes()
for i := 0; i < b.N; i++ {
ss := *(*string)(unsafe.Pointer(&bytes))
_ = ss
}
}
func BenchmarkSlice2StringUnsafe(b *testing.B) {
bytes := getBytes()
for i := 0; i < b.N; i++ {
ss := unsafe.String(unsafe.SliceData(bytes), len(bytes))
_ = ss
}
}