Go结构体SliceHeader及StringHeader作用详解

目录
  • 引言
  • SliceHeader
    • 疑问
  • StringHeader
  • 0 拷贝转换
  • 总结

引言

在 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 拷贝转换的场景进行了介绍和性能分析。

参考

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

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

(0)

相关推荐

  • GoFrame框架数据校验之校验对象校验结构体

    目录 前言摘要 基本概念 方法介绍 简要说明 注意问题TIPS 链式操作 示例1:单数据校验 简单示例 进阶示例 进阶示例打印结果 示例2:Map数据校验 示例3:Struct数据校验 总结 前言摘要 这篇文章将会为大家介绍GoFrame数据校验中校验对象的知识点,包括:Validator对象常用方法的介绍.单数据校验.校验Map.校验结构体的示例. 基本概念 数据校验组件提供了数据校验对象:用于数据校验统一的配置管理,支持我们便捷的进行链式操作. 方法介绍 type Validator fun

  • Go 语言结构体链表的基本操作

    目录 1. 什么是链表 2. 单项链表的基本操作 3. 使用 struct 定义单链表 4. 尾部添加节点方法一 5. 头部插入节点方法一 6. 指定节点后添加新节点 7. 删除节点 1. 什么是链表 链表是一种物理存储单元上非连续.非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的. 链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成.每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域. 使用链表结构可以避免在使用数组

  • Go语言学习函数+结构体+方法+接口

    目录 1. 函数 1.1 函数返回值 同一种类型返回值 带变量名的返回值 函数中的参数传递 函数变量 1.2 匿名函数——没有函数名字的函数 在定义时调用匿名函数 将匿名函数赋值给变量 匿名函数用作回调函数 可变参数——参数数量不固定的函数形式 1.3 闭包 1.4 defer语句 处理运行时发生的错误 1.5 宕机恢复(recover)——防止程序崩溃 2. 结构体 2.1 定义与给结构体赋值 3. 方法 3.1 结构体方法 3.2 接收器 指针接收器 非指针类型接收器 4. 接口 4.1 声

  • Go结合反射将结构体转换成Excel的过程详解

    目录 Excel中的一些概念 使用tealeg操作Excel 安装tealeg 使用tealeg新建一个表格 Go结合反射将结构体转换成Excel 反射获取每个Struct中的Tag 通过反射将结构体的值转换成map[excelTag]strucVal 利用反射将一个Silce,Array或者Struct转换成[]map[excelTag]strucVal 通过tealeg将[]map[excelTag]strucVal转换成Excel 运行测试用例验证 Excel中的一些概念 一个excel文

  • Go语言学习之结构体和方法使用详解

    目录 1. 结构体别名定义 2. 工厂模式 3. Tag 原信息 4. 匿名字段 5. 方法 1. 结构体别名定义 变量别名定义 package main import "fmt" type integer int func main() { //类型别名定义 var i integer = 1000 fmt.Printf("值: %d, 类型: %T\n", i, i) var j int = 100 j = int(i) //j和i不属于同一类型,需要转换 fm

  • golang中按照结构体的某个字段排序实例代码

    目录 概述 从大到小排序 按照结构体的某个字段排序 使用 sort.Stable 进行稳定排序 附:根据结构体中任意字段进行排序 总结 概述 golang的sort包默认支持int, float64, string的从小大到排序: int -> Ints(x []int)float64 -> Float64s(x []float64)string -> Strings(x []string) 同时它还提供了自定义的排序接口Interface,此接口保护三个方法. type Interfa

  • Go结构体SliceHeader及StringHeader作用详解

    目录 引言 SliceHeader 疑问 坑 StringHeader 0 拷贝转换 总结 引言 在 Go 语言中总是有一些看上去奇奇怪怪的东西,咋一眼一看感觉很熟悉,但又不理解其在 Go 代码中的实际意义,面试官却爱问... 今天要给大家介绍的是 SliceHeader 和 StringHeader 结构体,了解清楚他到底是什么,又有什么用,并且会在最后给大家介绍 0 拷贝转换的内容. 一起愉快地开始吸鱼之路. SliceHeader SliceHeader 如其名,Slice + Heade

  • 基于C#调用c++Dll结构体数组指针的问题详解

    C#调用c++dll文件是一件很麻烦的事情,首先面临的是数据类型转换的问题,相信经常做c#开发的都和我一样把学校的那点c++底子都忘光了吧(语言特性类). 网上有一大堆得转换对应表,也有一大堆的转换实例,但是都没有强调一个更重要的问题,就是c#数据类型和c++数据类型占内存长度的对应关系. 如果dll文件中只包含一些基础类型,那这个问题可能可以被忽略,但是如果是组合类型(这个叫法也许不妥),如结构体.类类型等,在其中的成员变量的长度的申明正确与否将决定你对dll文件调用的成败. 如有以下代码,其

  • C#中的只读结构体(readonly struct)详解

    翻译自 John Demetriou 2018年4月8日 的文章 <C# 7.2 – Let's Talk About Readonly Structs>[1] 在本文中,我们来聊一聊从 C# 7.2 开始出现的一个特性 readonly struct. 任一结构体都可以有公共属性.私有属性访问器等等.我们从以下结构体示例来开始讨论: public struct Person { public string Name { get; set; } public string Surname {

  • C语言自定义数据类型的结构体、枚举和联合详解

    结构体基础知识 首先结构体的出现是因为我们使用C语言的基本类型无法满足我们的需求,比如我们要描述一本书,就需要书名,作者,价格,出版社等等一系列的属性,无疑C语言的基本数据类型无法解决,所以就出现了最重要的自定义数据类型,结构体. 首先我们创建一个书的结构体类型来认识一下 struct Book { char name[20]; char author[20]; int price; }; 首先是struct是结构体关键字,用来告诉编译器你这里声明的是一个结构体类型而不是其他的东西,然后是Boo

  • C语言中的自定义类型之结构体与枚举和联合详解

    目录 1.结构体 1.1结构的基础知识 1.2结构的声明 1.3特殊的声明 1.4结构的自引用 1.5结构体变量的定义和初始化 1.6结构体内存对齐 1.7修改默认对齐数 1.8结构体传参 2.位段 2.1什么是位段 2.2位段的内存分配 2.3位段的跨平台问题 2.4位段的应用 3.枚举 3.1枚举类型的定义 3.2枚举的优点 3.3枚举的使用 4.联合 4.1联合类型的定义 4.2联合的特点 4.3联合大小的计算 1.结构体 1.1结构的基础知识 结构是一些值的集合,这些值称为成员变量.结构

  • goalng 结构体 方法集 接口实例详解

    目录 一 前序 二 事出有因 errors.As 方法签名 三 结构体与实例的数据结构 1. 结构体类型 2. 实例 3 方法调用 3.1 方法表达式 3.2 值实例调用所有方法 3.3 指针实例调用所有方法 3.4 空指针无法调用值方法 四 接口 1 接口数据结构 2 接口赋值 值方法集 指针方法集 总结 一 前序 很多时候我们以为自己懂了,但内心深处却偶有困惑,知识是严谨的,偶有困惑就是不懂,很幸运通过大量代码的磨练,终于看清困惑,并弄懂了. 本篇包括结构体,类型, 及 接口相关知识,希望对

  • Golang中结构体映射mapstructure库深入详解

    目录 mapstructure库 字段标签 内嵌结构 未映射字段 Metadata 弱类型输入 逆向转换 解码器 示例 在数据传递时,需要先编解码:常用的方式是JSON编解码(参见<golang之JSON处理>).但有时却需要读取部分字段后,才能知道具体类型,此时就可借助mapstructure库了. mapstructure库 mapstructure可方便地实现map[string]interface{}与struct间的转换:使用前,需要先导入库: go get github.com/m

  • 解析结构体的定义及使用详解

    结构的定义 定义一个结构的一般形式为: struct 结构名 { 成员表列 }成员表由若干个成员组成,每个成员都是该结构的一个组成部分.对每个成员也必须作类型说明. 例如: 复制代码 代码如下: struct stu { int num; char name[20]; int age; } 结构类型变量的说明结构体定义并不是定义一个变量,而是定义了一种数据类型,这种类型是你定义的,它可以和语言本身所自有的简单数据类型一样使用(如 int ).结构体本身并不会被作为数据而开辟内存,真正作为数据而在

  • Go interface接口声明实现及作用详解

    目录 什么是接口 接口的定义与作用 接口的声明和实现 接口的声明 接口的实现 接口类型断言 空接口 接口实际用途 通过接口实现面向对象多态特性 通过接口实现一个简单的 IoC (Inversion of Control) 什么是接口 接口是一种定义规范,规定了对象应该具有哪些方法,但并不指定这些方法的具体实现.在 Go 语言中,接口是由一组方法签名(方法名.参数类型.返回值类型)定义的.任何实现了这组方法的类型都可以被认为是实现了这个接口. 这种方式使得接口能够描述任意类型的行为,而不用关心其实

  • Mysql中explain作用详解

    一.MYSQL的索引 索引(Index):帮助Mysql高效获取数据的一种数据结构.用于提高查找效率,可以比作字典.可以简单理解为排好序的快速查找的数据结构. 索引的作用:便于查询和排序(所以添加索引会影响where 语句与 order by 排序语句). 在数据之外,数据库还维护着满足特定查找算法的数据结构,这些数据结构以某种方式引用数据.这样就可以在这些数据结构上实现高级查找算法.这些数据结构就是索引. 索引本身也很大,不可能全部存储在内存中,所以索引往往以索引文件的形式存储在磁盘上. 我们

随机推荐