本节内容主要代码位于 runtime/string.go
, 基于 go1.16.4/amd64
以下面的代码进行说明
1 2 3 slice := []byte {'h' , 'e' , 'l' , 'l' , 'o' } slice2str := string (slice) str2slice := []byte (slice2str)
slice to string 底层将 byte slice
转换成 string
的函数为 slicebytetostring
。
1 func slicebytetostring (buf *tmpBuf, ptr *byte , n int ) (str string )
这个函数接收三个参数,*tmpBuf
是一个指向 byte
数组的指针,ptr
指向 slice
第一个元素的地址, n
表示切片的长度。
函数执行的时候,首先会判断字符串的长度 n
是否为0,如果为0的话,直接返回空字符串。
如果切片的长度为 1
的话,那么会直接从一个 全局的静态表
中取出对应的数据,并且获取到指向这个元素指针,然后通过字符串的底层结构 stringStruct
进行赋值即可
1 2 3 4 5 6 7 8 9 if n == 1 { p := unsafe.Pointer(&staticuint64s[*ptr]) if sys.BigEndian { p = add(p, 7 ) } stringStructOf(&str).str = p stringStructOf(&str).len = 1 return }
staticuint64s
,位于 runtime/iface.go
是一个静态数组,可以避免给一些比较小的整数值分配空间
1 2 3 4 5 6 7 8 var staticuint64s = [...]uint64 { 0x00 , 0x01 , 0x02 , 0x03 , 0x04 , 0x05 , 0x06 , 0x07 , 0x08 , 0x09 , 0x0a , 0x0b , 0x0c , 0x0d , 0x0e , 0x0f , ..... 0xf0 , 0xf1 , 0xf2 , 0xf3 , 0xf4 , 0xf5 , 0xf6 , 0xf7 , 0xf8 , 0xf9 , 0xfa , 0xfb , 0xfc , 0xfd , 0xfe , 0xff , }
那么我们可以知道,对于长度为1个且值相同的 byte slice
,那么进行 string
转换的时候,对应的地址都是一样的,测试代码如下
1 2 3 4 5 6 test := []byte {'1' } test2 := []byte {'1' } a := string (test) b := string (test2) fmt.Printf("%x\n" , (*reflect.StringHeader)(unsafe.Pointer(&a)).Data) fmt.Printf("%x\n" ,(*reflect.StringHeader)(unsafe.Pointer(&b)).Data)
本地测试的时候输出均为 3e0a08
,也就验证了我们的想法
如果切片的长度大于1,那么首先优先使用 buf
作为底层的数组,如果长度不够的话,则使用 mallocgc
分配大小为 n
的空间
1 2 3 4 5 if buf != nil && n <= len (buf) { p = unsafe.Pointer(buf) } else { p = mallocgc(uintptr (n), nil , false ) }
分配好空间之后,进行赋值操作,最后调用 memmove
函数将 ptr
指向的n个字节的数据复制到申请的 p
中
1 2 3 stringStructOf(&str).str = p stringStructOf(&str).len = n memmove(p, unsafe.Pointer(ptr), uintptr (n))
所以主要的处理流程如下
string to slice stringtoslicebyte
负责将 string
类型转换成 byte slice
类型
1 func stringtoslicebyte (buf *tmpBuf, s string ) []byte
如果 string
的长度小于 buf
的长度,同时 buf
不为空,那么我们使用 buf
作为切片的存放空间,否则,我们需要调用 rawbyteslice
一块 len(s)
大小的 byte
切片大小空间,最后将s中的值复制到空间中返回这一块数据
1 2 3 4 5 6 7 8 var b []byte if buf != nil && len (s) <= len (buf) { *buf = tmpBuf{} b = buf[:len (s)] } else { b = rawbyteslice(len (s)) } copy (b, s)
rawbyteslice
其实也是调用 mallocgc
分配空间的,所以其实两者的转换在本质上都是申请一个空间,然后将数据拷贝一下,也没有什么特别神奇的地方,操作不同的具体原因就是 byte slice
和 string
类型的表示不太一样。
1 2 3 4 5 6 7 8 9 10 11 12 type stringStruct struct { str unsafe.Pointer len int } type slice struct { array unsafe.Pointer len int cap int }
当将 stringStruct
转换成 slice
的时候,我们需要将 str
指向的数据拷贝到 array
中 当将 slice
转换成 stringStruct
的时候,我们需要将 slice中array
指向的数据拷贝到 str
中
性能优化 通过上面的分析,我们知道在进行 byte slice
和 string
的转换的时候是会需要进行复制的,这个代价很大,需要重新分配空间,那么如果业务中我们对于一个 string
或者 byte slice
只需要进行读取操作,不要进行修改数据,那么可以通过强转进行实现,下面对每种转换进行了基准测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 func BenchmarkByte2String1 (b *testing.B) { bytes := []byte {'h' , 'e' , 'l' , 'l' , 'o' } for i := 0 ; i < b.N; i++ { _ = string (bytes) } } func BenchmarkByte2String2 (b *testing.B) { bytes := []byte {'h' , 'e' , 'l' , 'l' , 'o' } var s string for i := 0 ; i < b.N; i++ { (*reflect.StringHeader)(unsafe.Pointer(&s)).Data = (*reflect.SliceHeader)(unsafe.Pointer(&bytes)).Data (*reflect.StringHeader)(unsafe.Pointer(&s)).Len = len (bytes) } } func BenchmarkString2Byte1 (b *testing.B) { str := "hello" for i := 0 ; i < b.N; i++ { _ = []byte (str) } } func BenchmarkString2Byte2 (b *testing.B) { str := "hello" var bytes []byte for i := 0 ; i < b.N; i++ { (*reflect.SliceHeader)(unsafe.Pointer(&bytes)).Data = (*reflect.StringHeader)(unsafe.Pointer(&str)).Data (*reflect.SliceHeader)(unsafe.Pointer(&bytes)).Len = len (str) (*reflect.SliceHeader)(unsafe.Pointer(&bytes)).Cap = len (str) } }
运行 go test -bench . convert_test.go
结果
1 2 3 4 5 6 7 goos: linux goarch: amd64 cpu: Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz BenchmarkByte2String1-8 314203983 3.847 ns/op BenchmarkByte2String2-8 1000000000 0.2579 ns/op BenchmarkString2Byte1-8 225171763 5.340 ns/op BenchmarkString2Byte2-8 1000000000 0.2513 ns/op
由此可见,使用底层进行转换能够提高10多倍 性能,在不需要修改数据且对性能要求很高的情况下,可以考虑使用该种转换形式。