脚本之家,脚本语言编程技术及教程分享平台!
分类导航

Python|VBS|Ruby|Lua|perl|VBA|Golang|PowerShell|Erlang|autoit|Dos|bat|

服务器之家 - 脚本之家 - Golang - 搞 Go 要了解的 2 个 Header,你知道吗?

搞 Go 要了解的 2 个 Header,你知道吗?

2021-06-01 02:09脑子进煎鱼了陈煎鱼 Golang

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

搞 Go 要了解的 2 个 Header,你知道吗?

大家好,我是煎鱼。

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

今天要给大家介绍的是 SliceHeader 和 StringHeader 结构体,了解清楚他到底是什么,又有什么用,并且会在最后给大家介绍 0 拷贝转换的内容。

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

SliceHeader

 

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

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

既然知道了切片的运行时表现,那是不是就意味着我们可以自己造一个?

在日常程序中,可以利用标准库 reflect 提供的 SliceHeader 结构体造一个:

  1. func main() { 
  2.   // 初始化底层数组 
  3.  s := [4]string{"脑子""进""煎鱼""了"
  4.  s1 := s[0:1] 
  5.  s2 := s[:] 
  6.  
  7.   // 构造 SliceHeader 
  8.  sh1 := (*reflect.SliceHeader)(unsafe.Pointer(&s1)) 
  9.  sh2 := (*reflect.SliceHeader)(unsafe.Pointer(&s2)) 
  10.  fmt.Println(sh1.Len, sh1.Cap, sh1.Data) 
  11.  fmt.Println(sh2.Len, sh2.Cap, sh2.Data) 

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

输出结果:

  1. 1 4 824634330936 
  2.  
  3. 4 4 824634330936 

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

疑问

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

这其实是 Go 语言本身为了减少内存占用,提高整体的性能才这么设计的。

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

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

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

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

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

StringHeader

 

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

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

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

  1. var data = [...]byte{ 
  2.     'h''e''l''l''o'

底层的存储示意图如下:

搞 Go 要了解的 2 个 Header,你知道吗?

真实演示例子如下:

  1. func main() { 
  2.  s := "脑子进煎鱼了" 
  3.  s1 := "脑子进煎鱼了" 
  4.  s2 := "脑子进煎鱼了"[7:] 
  5.  
  6.  fmt.Printf("%d \n", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data) 
  7.  fmt.Printf("%d \n", (*reflect.StringHeader)(unsafe.Pointer(&s1)).Data) 
  8.  fmt.Printf("%d \n", (*reflect.StringHeader)(unsafe.Pointer(&s2)).Data) 

你认为输出结果是什么,变量 s 和 s1、s2 会指向同一个底层内存空间吗?

输出结果:

  1. 17608227  
  2. 17608227  
  3. 17608234  

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

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

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

0 拷贝转换

 

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

常见转换代码如下:

  1. func string2bytes(s string) []byte { 
  2.  stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) 
  3.  
  4.  bh := reflect.SliceHeader{ 
  5.   Data: stringHeader.Data, 
  6.   Len:  stringHeader.Len, 
  7.   Cap:  stringHeader.Len, 
  8.  } 
  9.  
  10.  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)。

应该使用如下转换方式:

  1. func main() { 
  2.  s := "脑子进煎鱼了" 
  3.  v := string2bytes1(s) 
  4.  fmt.Println(v) 
  5.  
  6. func string2bytes1(s string) []byte { 
  7.  stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&s)) 
  8.  
  9.  var b []byte 
  10.  pbytes := (*reflect.SliceHeader)(unsafe.Pointer(&b)) 
  11.  pbytes.Data = stringHeader.Data 
  12.  pbytes.Len = stringHeader.Len 
  13.  pbytes.Cap = stringHeader.Len 
  14.  
  15.  return b 

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

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

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

性能对比:

  1. string2bytes1-1000-4   3.746 ns/op  0 allocs/op 
  2. string2bytes1-1000-4   3.713 ns/op  0 allocs/op 
  3. string2bytes1-1000-4   3.969 ns/op  0 allocs/op 
  4.  
  5. string2bytes2-1000-4   2.445 ns/op  0 allocs/op 
  6. string2bytes2-1000-4   2.451 ns/op  0 allocs/op 
  7. string2bytes2-1000-4   2.455 ns/op  0 allocs/op 

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

代码如下:

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

输出结果:

  1. 18 824633927632 

这种强转其会导致 byte 的切片容量非常大,需要特别注意。一般还是推荐使用标准的 SliceHeader、StringHeader 方式就好了,也便于后来的维护者理解。

总结

 

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

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

最后我们进一步深入,面向 0 拷贝转换的场景进行了介绍和性能分析。

你平时有没有遇到过这块的疑惑或问题呢,欢迎大家一起讨论!

参考

Go语言slice的本质-SliceHeader

数组、字符串和切片

零拷贝实现string 和bytes的转换疑问

原文链接:https://mp.weixin.qq.com/s/rGqM1wMlqQEoJSgyrgZNLg

延伸 · 阅读

精彩推荐
  • GolangGolang中Bit数组的实现方式

    Golang中Bit数组的实现方式

    这篇文章主要介绍了Golang中Bit数组的实现方式,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    天易独尊11682021-06-09
  • Golanggolang如何使用struct的tag属性的详细介绍

    golang如何使用struct的tag属性的详细介绍

    这篇文章主要介绍了golang如何使用struct的tag属性的详细介绍,从例子说起,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看...

    Go语言中文网11352020-05-21
  • Golanggolang json.Marshal 特殊html字符被转义的解决方法

    golang json.Marshal 特殊html字符被转义的解决方法

    今天小编就为大家分享一篇golang json.Marshal 特殊html字符被转义的解决方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧 ...

    李浩的life12792020-05-27
  • Golanggolang的httpserver优雅重启方法详解

    golang的httpserver优雅重启方法详解

    这篇文章主要给大家介绍了关于golang的httpserver优雅重启的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,...

    helight2992020-05-14
  • Golanggolang 通过ssh代理连接mysql的操作

    golang 通过ssh代理连接mysql的操作

    这篇文章主要介绍了golang 通过ssh代理连接mysql的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    a165861639710342021-03-08
  • Golanggo语言制作端口扫描器

    go语言制作端口扫描器

    本文给大家分享的是使用go语言编写的TCP端口扫描器,可以选择IP范围,扫描的端口,以及多线程,有需要的小伙伴可以参考下。 ...

    脚本之家3642020-04-25
  • Golanggo日志系统logrus显示文件和行号的操作

    go日志系统logrus显示文件和行号的操作

    这篇文章主要介绍了go日志系统logrus显示文件和行号的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...

    SmallQinYan12302021-02-02
  • GolangGolang通脉之数据类型详情

    Golang通脉之数据类型详情

    这篇文章主要介绍了Golang通脉之数据类型,在编程语言中标识符就是定义的具有某种意义的词,比如变量名、常量名、函数名等等,Go语言中标识符允许由...

    4272021-11-24