热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Go结构体SliceHeader及StringHeader作用详解【golang面试】

这篇文章主要为大家介绍了Go结构体SliceHeader及StringHeader作用的功能及面试官爱问的实际意义详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多

引言

在 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 
", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)
 fmt.Printf("%d 
", (*reflect.StringHeader)(unsafe.Pointer(&s1)).Data)
 fmt.Printf("%d 
", (*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 拷贝转换的场景进行了介绍和性能分析。

参考

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

以上就是Go结构体SliceHeader及StringHeader作用详解的详细内容,更多关于Go结构体SliceHeader StringHeader的资料请关注编程笔记其它相关文章!


推荐阅读
  • 本文详细介绍了Java中vector的使用方法和相关知识,包括vector类的功能、构造方法和使用注意事项。通过使用vector类,可以方便地实现动态数组的功能,并且可以随意插入不同类型的对象,进行查找、插入和删除操作。这篇文章对于需要频繁进行查找、插入和删除操作的情况下,使用vector类是一个很好的选择。 ... [详细]
  • 本文介绍了使用Java实现大数乘法的分治算法,包括输入数据的处理、普通大数乘法的结果和Karatsuba大数乘法的结果。通过改变long类型可以适应不同范围的大数乘法计算。 ... [详细]
  • 本文介绍了C#中生成随机数的三种方法,并分析了其中存在的问题。首先介绍了使用Random类生成随机数的默认方法,但在高并发情况下可能会出现重复的情况。接着通过循环生成了一系列随机数,进一步突显了这个问题。文章指出,随机数生成在任何编程语言中都是必备的功能,但Random类生成的随机数并不可靠。最后,提出了需要寻找其他可靠的随机数生成方法的建议。 ... [详细]
  • 本文介绍了如何将CIM_DateTime解析为.Net DateTime,并分享了解析过程中可能遇到的问题和解决方法。通过使用DateTime.ParseExact方法和适当的格式字符串,可以成功解析CIM_DateTime字符串。同时还提供了关于WMI和字符串格式的相关信息。 ... [详细]
  • 本文介绍了基于c语言的mcs51单片机定时器计数器的应用教程,包括定时器的设置和计数方法,以及中断函数的使用。同时介绍了定时器应用的举例,包括定时器中断函数的编写和频率值的计算方法。主函数中设置了T0模式和T1计数的初值,并开启了T0和T1的中断,最后启动了CPU中断。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 本文介绍了如何在给定的有序字符序列中插入新字符,并保持序列的有序性。通过示例代码演示了插入过程,以及插入后的字符序列。 ... [详细]
  • Android Studio Bumblebee | 2021.1.1(大黄蜂版本使用介绍)
    本文介绍了Android Studio Bumblebee | 2021.1.1(大黄蜂版本)的使用方法和相关知识,包括Gradle的介绍、设备管理器的配置、无线调试、新版本问题等内容。同时还提供了更新版本的下载地址和启动页面截图。 ... [详细]
  • 本文介绍了一种划分和计数油田地块的方法。根据给定的条件,通过遍历和DFS算法,将符合条件的地块标记为不符合条件的地块,并进行计数。同时,还介绍了如何判断点是否在给定范围内的方法。 ... [详细]
  • 本文介绍了Oracle数据库中tnsnames.ora文件的作用和配置方法。tnsnames.ora文件在数据库启动过程中会被读取,用于解析LOCAL_LISTENER,并且与侦听无关。文章还提供了配置LOCAL_LISTENER和1522端口的示例,并展示了listener.ora文件的内容。 ... [详细]
  • 本文介绍了P1651题目的描述和要求,以及计算能搭建的塔的最大高度的方法。通过动态规划和状压技术,将问题转化为求解差值的问题,并定义了相应的状态。最终得出了计算最大高度的解法。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 猜字母游戏
    猜字母游戏猜字母游戏——设计数据结构猜字母游戏——设计程序结构猜字母游戏——实现字母生成方法猜字母游戏——实现字母检测方法猜字母游戏——实现主方法1猜字母游戏——设计数据结构1.1 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
author-avatar
吴尧丹_449
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有