关于golang:不得不知的-Go-SliceHeader-和-StringHeader你知道吗

68次阅读

共计 3932 个字符,预计需要花费 10 分钟才能阅读完成。

大家好,我是煎鱼。

在 Go 语言中总是有一些看上去奇奇怪怪的货色,咋一眼一看感觉很相熟,但又不了解其在 Go 代码中的实际意义,面试官却爱问 …

明天要给大家介绍的是 SliceHeader 和 StringHeader 构造体,理解分明他到底是什么,又有什么用,并且会在最初给大家介绍 0 拷贝转换的内容。

一起欢快地开始吸鱼之路。

SliceHeader

SliceHeader 如其名,Slice + Header,看上去很直观,实际上是 Go Slice(切片)的运行时体现。

SliceHeader 的定义如下:

type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
}
  • Data:指向具体的底层数组。
  • Len:代表切片的长度。
  • Cap:代表切片的容量。

既然晓得了切片的运行时体现,那是不是就意味着咱们能够本人造一个?

在日常程序中,能够利用规范库 reflect 提供的 SliceHeader 构造体造一个:

func main() {
  // 初始化底层数组
 s := [4]string{"脑子", "进", "煎鱼", "了"}
 s1 := s[0:1]
 s2 := s[:]

  // 结构 SliceHeader
 sh1 := (*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)
}

你认为输入后果是什么,这两个新切片会指向同一个底层数组的内存地址吗?

输入后果:

1 4 824634330936
4 4 824634330936

两个切片的 Data 属性所指向的底层数组是统一的,Len 属性的值不一样,sh1 和 sh2 别离是两个切片。

疑难

为什么两个新切片所指向的 Data 是同一个地址的呢?

这其实是 Go 语言自身为了缩小内存占用,进步整体的性能才这么设计的。

将切片复制到任意函数的时候,对底层数组大小都不会影响。复制时只会复制切片自身(值传递),不会波及底层数组。

也就是在函数间传递切片,其只拷贝 24 个字节(指针字段 8 个字节,长度和容量别离须要 8 个字节),效率很高。

这种设计也引出了新的问题,在平时通过 s[i:j] 所生成的新切片,两个切片底层指向的是同一个底层数组。

假如 在没有超过容量(cap)的状况下,对第二个切片操作会影响第一个切片

这是很多 Go 开发常会碰到的一个大“坑”,不分明的排查了很久的都不得而终。

StringHeader

除了 SliceHeader 外,Go 语言中还有一个典型代表,那就是字符串(string)的运行时体现。

StringHeader 的定义如下:

type StringHeader struct {
   Data uintptr
   Len  int
}
  • Data:寄存指针,其指向具体的存储数据的内存区域。
  • Len:字符串的长度。

可得悉“Hello”字符串的底层数据如下:

var data = [...]byte{'h', 'e', 'l', 'l', 'o',}

底层的存储示意图如下:

图来自网络

实在演示例子如下:

func main() {
 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 会指向同一个底层内存空间吗?

输入后果:

17608227 
17608227 
17608234 

从输入后果来看,变量 s 和 s1 指向同一个内存地址。变量 s2 虽稍有偏差,但实质上也是指向同一块。

因为其是字符串的切片操作,是从第 7 位索引开始,因而正好的 17608234-17608227 = 7。也就是三个变量都是指向同一块内存空间,这是为什么呢?

这是因为在 Go 语言中,字符串都是只读的,为了节俭内存,雷同字面量的字符串通常对应于同一字符串常量,因而指向同一个底层数组

0 拷贝转换

为什么会有人关注到 SliceHeader、StringHeader 这类运行时细节呢,一大部分起因是业内会有开发者,心愿利用其实现零拷贝的 string 到 bytes 的转换

常见转换代码如下:

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))
}

但这其实是谬误的,官网明确示意:

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.

SliceHeader、StringHeader 的 Data 字段是一个 uintptr 类型。因为 Go 语言只有值传递。

因而在上述代码中会呈现将 Data 作为值拷贝的状况,这就会导致 无奈保障它所援用的数据不会被垃圾回收(GC)

应该应用如下转换形式:

func main() {
 s := "脑子进煎鱼了"
 v := string2bytes1(s)
 fmt.Println(v)
}

func string2bytes1(s string) []byte {stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s))

 var b []byte
 pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b))
 pbytes.Data = stringHeader.Data
 pbytes.Len = stringHeader.Len
 pbytes.Cap = stringHeader.Len

 return b
}

在程序必须保留一个独自的、正确类型的指向底层数据的指针。

在性能方面,若只是冀望单纯的转换,对容量(cap)等字段值不敏感,也能够应用以下形式:

func string2bytes2(s string) []byte {return *(*[]byte)(unsafe.Pointer(&s))
}

性能比照:

string2bytes1-1000-4   3.746 ns/op  0 allocs/op
string2bytes1-1000-4   3.713 ns/op  0 allocs/op
string2bytes1-1000-4   3.969 ns/op  0 allocs/op

string2bytes2-1000-4   2.445 ns/op  0 allocs/op
string2bytes2-1000-4   2.451 ns/op  0 allocs/op
string2bytes2-1000-4   2.455 ns/op  0 allocs/op

会相当规范的转换性能会稍快一些,这种强转也会导致一个小问题。

代码如下:

func main() {
 s := "脑子进煎鱼了"
 v := string2bytes2(s)
 println(len(v), cap(v))
}
func string2bytes2(s string) []byte {return *(*[]byte)(unsafe.Pointer(&s))
}

输入后果:

18 824633927632

这种强转其会导致 byte 的切片容量十分大,须要特地留神。个别还是举荐应用规范的 SliceHeader、StringHeader 形式就好了,也便于起初的维护者了解。

总结

在这篇文章中,咱们介绍了字符串(string)和切片(slice)的两个运行时体现,别离是 StringHeader 和 SliceHeader。

同时理解到其运行时体现后,咱们还针对其两者的地址指向,常见坑进行了阐明。

最初咱们进一步深刻,面向 0 拷贝转换的场景进行了介绍和性能剖析。

你平时有没有遇到过这块的纳闷或问题呢,欢送大家一起探讨!

若有任何疑难欢送评论区反馈和交换,最好的关系是相互成就 ,各位的 点赞 就是煎鱼创作的最大能源,感激反对。

文章继续更新,能够微信搜【脑子进煎鱼了】浏览,本文 GitHub github.com/eddycjy/blog 已收录,学习 Go 语言能够看 Go 学习地图和路线,欢送 Star 催更。

参考

  • Go 语言 slice 的实质 -SliceHeader
  • 数组、字符串和切片
  • 零拷贝实现 string 和 bytes 的转换疑难

正文完
 0