大家好,我是炸鱼。Go语言中总有一些奇怪的东西。乍一看感觉很眼熟,但不明白它们在Go代码中的实际含义。面试官爱问...今天要给大家介绍的是SliceHeader和StringHeader的结构。了解它是什么,有什么用,最后会为大家介绍一下0副本转换的内容。让我们一起愉快地开启熏鱼之路吧。SliceHeaderSliceHeader,顾名思义,Slice+Header,看起来很直观,但实际上是GoSlice(切片)的运行时表现。SliceHeader的定义如下:typeSliceHeaderstruct{DatauintptrLenintCapint}Data:指向具体的底层数组。Len:表示切片的长度。Cap:表示切片的容量。现在我们知道了切片的运行时性能,是否意味着我们可以自己创建一个?在日常程序中,可以使用标准库reflect提供的SliceHeader结构来创建一个:funcmain(){//初始化底层数组s:=[4]string{"brain","in","friedfish",""}s1:=s[0:1]s2:=s[:]//构造SliceHeadersh1:=(*reflect.SliceHeader)(unsafe.Pointer(&s1))sh2:=(*reflect.SliceHeader)(unsafe.Pointer(&s2))fmt.Println(sh1.Len,sh1.Cap,sh1.Data)fmt.Println(sh2.Len,sh2.Cap,sh2.Data)}你认为输出是什么,这两个新的slice会不会指向同一个底层数组的内存地址呢?输出结果:1482463433093644824634330936两个切片的Data属性指向的底层数组相同,但Len属性的值不同,sh1和sh2分别是两个切片。疑问为什么两个新切片指向的Data在同一个地址?这其实是Go语言自己为了减少内存占用,提高整体性能而设计的。将切片复制到任何函数都不会影响底层数组的大小。复制时,只复制切片本身(按值传递),不触及底层数组。也就是说,在函数间传递切片时,只复制24个字节(指针字段8个字节,长度和容量各8个字节),效率很高。这种坑的设计也导致了新的问题。在平时s[i:j]生成的新切片中,两个切片的底层指向同一个底层数组。假设在不超过容量(cap)的情况下,对第二个切片的操作会影响第一个切片。这是很多Go开发者经常遇到的一个大“坑”,不知不觉就被查了很久。StringHeader除了SliceHeader,Go语言中还有一个典型的代表,就是字符串的运行时表现。StringHeader的定义如下:typeStringHeaderstruct{DatauintptrLenint}Data:存储指针,指向存储数据的具体内存区域。Len:字符串的长度。可以知道,“你好”字符串的底层数据如下:vardata=[...]byte{'h','e','l','l','o',}底层存储图如下:图片来自网络真实演示示例如下:funcmain(){s:="脑子炸了"s1:="脑子炸了"s2:="脑子炸了"[7:]fmt.Printf("%d\n",(*reflect.StringHeader)(unsafe.Pointer(&s)).Data)fmt.Printf("%d\n",(*reflect.StringHeader)(unsafe.Pointer(&s1)).Data)fmt.Printf("%d\n",(*reflect.StringHeader)(unsafe.Pointer(&s2)).Data)}你认为输出结果如何将?变量s、s1、s2会指向同一个底层内存空间?输出结果:176082271760822717608234从输出结果看,变量s和s1指向同一个内存地址。尽管变量s2略有偏差,但它基本上指向同一个块。因为是字符串切片操作,所以从第7个索引开始,所以恰好17608234-17608227=7。即三个变量都指向同一个内存空间。为什么是这样?这是因为在Go语言中,字符串都是只读的。为了节省内存,字面值相同的字符串通常对应同一个字符串常量,因此指向同一个底层数组。0复制转换为什么人们会关注SliceHeader和StringHeader等运行时细节?多半是因为业内有开发者希望利用它来实现字符串到字节的零拷贝转换。常用的转换代码如下:funcstring2bytes(sstring)[]byte{stringHeader:=(*reflect.StringHeader)(unsafe.Pointer(&s))bh:=reflect.SliceHeader{Data:stringHeader.Data,Len:stringHeader.Len,Cap:stringHeader.Len,}return*(*[]byte)(unsafe.Pointer(&bh))}但这其实是错误的,官方明确说明:Data字段不足以保证其引用的数据不会被垃圾收集,因此程序必须保留一个单独的、正确类型的指向底层数据的指针。SliceHeader和StringHeader的Data域是uintptr类型。由于Go语言只按值传递。因此,在上面的代码中,Data会被复制为一个值,这样就无法保证它所引用的数据不会被垃圾回收(GC)。应使用以下转换方法:funcmain(){s:="大脑在煎鱼"v:=string2bytes1(s)fmt.Println(v)}funcstring2bytes1(sstring)[]byte{stringHeader:=(*reflect.StringHeader)(unsafe.Pointer(&s))varb[]bytepbytes:=(*reflect.SliceHeader)(unsafe.Pointer(&b))pbytes.Data=stringHeader.Datapbytes.Len=stringHeader.Len字节。Cap=stringHeader.Lenreturnb}程序必须保留一个指向基础数据的正确类型的指针。在性能方面,如果只期望简单的转换,对容量(cap)等字段值不敏感,也可以使用如下方法:funcstring2bytes2(sstring)[]byte{return*(*[]byte)(unsafe.Pointer(&s))}性能比较:string2bytes1-1000-43.746ns/op0allocs/opstring2bytes1-1000-43.713ns/op0allocs/opstring2bytes1-1000-43.969ns/op0allocs/opstring2bytes4-452.2。ns/op0allocs/opstring2bytes2-1000-42.451ns/op0allocs/opstring2bytes2-1000-42.455ns/op0allocs/op将是相当标准的转换性能会稍微快一些,这种强制传输也会导致小菜一碟。代码如下:funcmain(){s:="Mybrainisfryingfish"v:=string2bytes2(s)println(len(v),cap(v))}funcstring2bytes2(sstring)[]byte{return*(*[]byte)(unsafe.Pointer(&s))}输出结果:18824633927632这种强制转换会导致byteslice容量非常大,需要特别注意。一般推荐使用标准的SliceHeader和StringHeader方法,也便于后期维护者理解。总结在本文中,我们介绍了字符串和切片的两种运行时表示,即StringHeader和SliceHeader。同时,在了解了它的运行时性能后,我们也讲解了两者的地址指针和常见陷阱。最后,我们更进一步,对0-copy转换场景的性能进行了介绍和分析。您是否遇到过这方面的疑惑或问题?欢迎一起讨论!如有任何问题,欢迎在评论区反馈交流。最好的关系是相互成就。您的好评是创作炸鱼最大的动力。感谢您的支持。文章持续更新中。可以微信搜索【脑补炸鱼】阅读。本文已收录在GitHubgithub.com/eddycjy/blog中。学习Go语言可以看Go学习地图和路线。欢迎星星提醒。参考Go语言slice精髓——SliceHeader数组、字符串和slice零拷贝实现字符串和字节的转换
