前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Go通关18:SliceHeader,slice 如何高效处理数据?

Go通关18:SliceHeader,slice 如何高效处理数据?

作者头像
微客鸟窝
发布于 2021-08-18 07:22:45
发布于 2021-08-18 07:22:45
61810
代码可运行
举报
文章被收录于专栏:Go语言指北Go语言指北
运行总次数:0
代码可运行

数组

Go 语言中,数组类型包括两部分:「数组大小、数组内部元素类型」

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
a1 := [1]string("微客鸟窝")
a2 := [2]string("微客鸟窝")

示例中变量 a1 的类型是 [1]string,变量 a2 的类型是 [2]string,因为它们大小不一致,所以不是同一类型。

数组局限性

  • 数组被声明之后,它的大小和内部元素的类型就不能再被改变
  • 因为在 Go 语言中,函数之间的参数传递是「值传递」,数组作为参数的时候,会将其复制一份,如果它非常大,「会造成大量的内存浪费」

正是因为数组有这些局限性,Go 又设计了 slice !

slice 切片

slice 切片的底层数据是存储在数组中的,可以说是数组的改良版,slice 是对数组的抽象和封装,它可以动态的添加元素,容量不足时可以自动扩容。

动态扩容

通过内置的 append 方法,可以对切片追加任意多个元素:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
  s := []string{"微客鸟窝","无尘"}
  s = append(s,"wucs")
  fmt.Println(s) //[微客鸟窝 无尘 wucs]
}

append 方法追加元素时,如果切片的容量不够,会自动进行扩容:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
   s := []string{"微客鸟窝","无尘"}
   fmt.Println("切片长度:",len(s),";切片容量:",cap(s))
   s = append(s,"wucs")
   fmt.Println("切片长度:",len(s),";切片容量:",cap(s))
   fmt.Println(s) //
}

运行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
切片长度: 2 ;切片容量: 2
切片长度: 3 ;切片容量: 4
[微客鸟窝 无尘 wucs]

通过运行结果我们发现,在调用 append 之前,容量是 2,调用之后容量是 4,说明自动扩容了。 扩容原理是新建一个底层数组,把原来切片内的元素拷贝到新的数组中,然后返回一个指向新数组的切片。

切片结构体

切片其实是一个结构体,它的定义如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type SliceHeader struct {
   Data uintptr
   Len  int
   Cap  int
}
  • Data 用来指向存储切片元素的数组。
  • Len 代表切片的长度。
  • Cap 代表切片的容量。通过这三个字段,就可以把一个数组抽象成一个切片,所以不同切片对应的底层 Data 指向的可能是同一个数组。 示例:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
 a1 := [2]string{"微客鸟窝","无尘"}
 s1 := a1[0:1]
 s2 := a1[:]
 fmt.Println((*reflect.SliceHeader)(unsafe.Pointer(&s1)).Data)
 fmt.Println((*reflect.SliceHeader)(unsafe.Pointer(&s2)).Data)
}

运行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
824634892120
824634892120

我们发现打印出s1和s2的Data值是一样的,说明两个切片共用一个数组。所以在对切片进行操作时,使用的还是同一个数组,没有复制原来的元素,减少内存的占用,提高效率。 多个切片共用一个底层数组虽然可以减少内存占用,但是如果一个切片修改了内部元素,其他切片也会受到影响,所以切片作为参数传递的时候要小心,尽可能不要修改远切片内的元素。切片的本质是 SliceHeader,又因为函数的参数是值传递,所以传递的是 SliceHeader 的副本,而不是底层数组的副本,这样就可以大大减少内存的使用。 获取切片数组结果的三个字段的值,除了使用 SliceHeader,也可以自定义一个结构体,只有包子字段和 SliceHeader 一样就可以了:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
 s := []string{"微客鸟窝","无尘","wucs"}
 s1 := (*any)(unsafe.Pointer(&s))
 fmt.Println(s1.Data,s1.Len,s1.Cap) //824634892104 3 3
}
type any struct {
 Data uintptr
 Len int
 Cap int
}

高效

对于Go 语言中的集合类型:数组、切片、map,数组和切片的取值和赋值操作相比 map 要更高效,因为它们是连续的内存操作,可以通过索引就能快速地找到元素存储的地址。在函数传参中,切片相比数组要高效,因为切片作为参数,不会把所有的元素都复制一遍,只是复制 SliceHeader 的三个字段,共用的仍是同一个底层数组。 示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
 a := [2]string{"微客鸟窝", "无尘"}
 fmt.Printf("函数main数组指针:%p\n", &a)

 arrayData(a)
 s := a[0:1]
 fmt.Println((*reflect.SliceHeader)(unsafe.Pointer(&s)).Data)
 sliceData(s)
}
func arrayData(a [2]string) {
 fmt.Printf("函数arrayData数组指针:%p\n", &a)
}
func sliceData(s []string) {
 fmt.Println("函数sliceData数组指针:", (*reflect.SliceHeader)(unsafe.Pointer(&s)).Data)
}

运行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
函数main数组指针:0xc0000503c0
函数arrayData数组指针:0xc000050400
824634049472
函数sliceData数组指针: 824634049472

可以发现:

  • 同一个数组传到 arrayData 函数中指针发生了变化,说明数组在传参的时候被复制了,产生了一个新的数组。
  • 切片作为参数传递给 sliceData 函数,指针没有发生变化,因为 slice 切片的底层 Data 是一样的,切片共用的是一个底层数组,底层数组没有被复制。

string 和 []byte 互转

string 底层结构 StringHeader:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// StringHeader is the runtime representation of a string.
type StringHeader struct {
   Data uintptr
   Len  int
}

StringHeader 和 SliceHeader 一样,代表的是字符串在程序运行时的真实结构,可以看到字段仅比切片少了一个Cap属性。 []byte(s) 和 string(b) 强制转换:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
   s := "微客鸟窝"
   fmt.Printf("s的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)
   b := []byte(s)
   fmt.Printf("b的内存地址:%d\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)
   c := string(b)
   fmt.Printf("c的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&c)).Data)
}

运行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
s的内存地址:8125426
b的内存地址:824634892016
c的内存地址:824634891984

通过上面示例发现打印出的内存地址都不一样,可以看出[]byte(s) 和 string(b) 这种强制转换会重新拷贝一份字符串。若字符串非常大,这样重新拷贝的方式会很影响性能。

优化

[]byte 转 string,就等于通过 unsafe.Pointer 把 *SliceHeader 转为 *StringHeader,也就是 *[]byte 转 *string。 零拷贝示例:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
 s := "微客鸟窝"
 fmt.Printf("s的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)
 b := []byte(s)
 fmt.Printf("b的内存地址:%d\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)
 //c1 :=string(b)
 c2 := *(*string)(unsafe.Pointer(&b))
 fmt.Printf("c2的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&c2)).Data)
}

运行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
s的内存地址:1899506
b的内存地址:824634597104
c2的内存地址:824634597104

示例中,c1 和 c2 的内容是一样的,不一样的是 c2 没有申请新内存(零拷贝),c2 和变量b使用的是同一块内存,因为它们的底层 Data 字段值相同,这样就节约了内存,也达到了 []byte 转 string 的目的。 SliceHeader 有 Data、Len、Cap 三个字段,StringHeader 有 Data、Len 两个字段,所以 *SliceHeader 通过 unsafe.Pointer 转为 *StringHeader 的时候没有问题,但是反过来却不行了,因为 *StringHeader 缺少 *SliceHeader 所需的 Cap 字段,需要我们自己补上一个默认值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func main() {
 s := "微客鸟窝"
 fmt.Printf("s的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&s)).Data)
 b := []byte(s)
 fmt.Printf("b的内存地址:%d\n", (*reflect.SliceHeader)(unsafe.Pointer(&b)).Data)
 sh := (*reflect.SliceHeader)(unsafe.Pointer(&s))
 sh.Cap = sh.Len
 b1 := *(*[]byte)(unsafe.Pointer(sh))
 fmt.Printf("b1的内存地址:%d\n", (*reflect.StringHeader)(unsafe.Pointer(&b1)).Data)
}

运行结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
s的内存地址:1309682
b的内存地址:824634892008
b1的内存地址:1309682
  1. b1 和 b 的内容是一样的,不一样的是 b1 没有申请新内存,而是和变量 s 使用同一块内存,因为它们底层的 Data 字段相同,所以也节约了内存。
  2. 通过 unsafe.Pointer 把 string 转为 []byte 后,不能对 []byte 修改,比如不可以进行 b1[0]=10 这种操作,会报异常,导致程序崩溃。因为在 Go 语言中 string 内存是只读的。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 微客鸟窝 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
1 条评论
热度
最新
最后一段为啥不直接转换呢?出于直觉有首地址和类型就知道读取多少字节,数据解析应该没问题才对,虽然没有cap。 d1 := *(*[]byte)(unsafe.Pointer(&s)) fmt.Printf("d1的内存地址:%d\n", &d1[0])
最后一段为啥不直接转换呢?出于直觉有首地址和类型就知道读取多少字节,数据解析应该没问题才对,虽然没有cap。 d1 := *(*[]byte)(unsafe.Pointer(&s)) fmt.Printf("d1的内存地址:%d\n", &d1[0])
回复回复点赞举报
推荐阅读
编辑精选文章
换一批
Go语言的引用类型
Go中的引用类型不是指针,而是对指针的包装,在它的内部通过指针引用底层数据结构。每一种引用类型也包含一些其他的field,用来管理底层的数据结构。
mazhen
2023/11/24
2380
Go语言的引用类型
Go指针的使用限制和突破之路
大家好呀,今天网管想在这篇文章里好好跟大家聊一下 Go 语言指针这个话题,相较于 C 而言,Go 语言在设计时为了使用安全给指针在类型和运算上增加了限制,这让Go程序员既可以享受指针带来的便利,又避免了指针的危险性。除了常规的指针外,Go 语言在 unsafe 包里其实还通过 unsafe.Pointer 提供了通用指针,通过这个通用指针以及 unsafe 包的其他几个功能又让使用者能够绕过 Go 语言的类型系统直接操作内存进行例如:指针类型转换,读写结构体私有成员这样操作。网管觉得正是因为功能强大同时伴随着操作不慎读写了错误的内存地址即会造成的严重后果所以 Go 语言的设计者才会把这些功能放在 unsafe 包里。其实也没有想得那么不安全,掌握好了使用得当还是能带来很大的便利的,在一些偏向底层的源码中 unsafe 包使用的频率还是不低的。对于励志成为高阶 Gopher 的各位,这也是一项必不可少需要掌握的技能啦。接下来网管就带大家从基本的指针使用方法和限制开始看看怎么用 unsafe 包跨过这些限制直接读写内存。
KevinYan
2021/01/13
1K0
[译] Go 1.20 新变化!第一部分:语言特性
又到了 Go 发布新版本的时刻了!2022 年第一季度的 Go 1.18 是一个主版本,它在语言中增加了期待已久的泛型,同时还有许多微小功能更新与优化。2022 年第三季度的 Go 1.19 是一个比较低调的版本。现在是 2023 年,Go 1.20 RC 版本已经发布,而正式版本也即将到来,Go 团队已经发布了版本说明草案。
pseudoyu
2023/04/11
9060
深入解析 Go 中 Slice 底层实现
切片是 Go 中的一种基本的数据结构,使用这种结构可以用来管理数据集合。切片的设计想法是由动态数组概念而来,为了开发者可以更加方便的使一个数据结构可以自动增加和减少。但是切片本身并不是动态数据或者数组指针。切片常见的操作有 reslice、append、copy。与此同时,切片还具有可索引,可迭代的优秀特性。
李海彬
2018/07/26
8940
深入解析 Go 中 Slice 底层实现
golang string和[]byte的对比
为啥string和[]byte类型转换需要一定的代价? 为啥内置函数copy会有一种特殊情况copy(dst []byte, src string) int? string和[]byte,底层都是数组
sunsky
2020/08/19
4.3K0
连nil切片和空切片一不一样都不清楚?那BAT面试官只好让你回去等通知了。
问题 package main import ( "fmt" "reflect" "unsafe" ) func main() { var s1 []int s2 := make([]int,0) s4 := make([]int,0) fmt.Printf("s1 pointer:%+v, s2 pointer:%+v, s4 pointer:%+v, \n", *(*reflect.SliceHeader)(unsafe.Pointer(&s1)),*(*reflect.Sli
9号同学
2021/03/03
2930
连nil切片和空切片一不一样都不清楚?那BAT面试官只好让你回去等通知了。
Golang切片与实现原理
array是切片用来存储数据的底层数组的指针,len为切片中元素的数量,cap为切片的容量即数组的长度
Orlion
2024/09/02
810
Golang切片与实现原理
Golang指针与unsafe
我们知道在golang中是存在指针这个概念的。对于指针很多人有点忌惮(可能是因为之前学习过C语言),因为它会导致很多异常的问题。但是很多人学习之后发现,golang中的指针很简单,没有C那么复杂。所以今天就详细来说说指针。
LinkinStar
2022/09/01
2910
Go的atomic.Value为什么不加锁也能保证数据线程安全?
有些朋友可能没有注意过,在 Go(甚至是大部分语言)中,一条普通的赋值语句其实不是一个原子操作。例如,在32位机器上写int64类型的变量就会有中间状态,因为它会被拆成两次写操作(汇编的MOV指令)——写低 32 位和写高 32 位,如下图所示:
KevinYan
2021/11/18
1.2K0
Go的atomic.Value为什么不加锁也能保证数据线程安全?
聊一个string和[]byte转换问题
前几天闲聊的时候,景埕说网上很多 string 和 []byte 的转换都是有问题的,当时并没有在意,转过身没几天我偶然看到字节跳动的一篇文章,其中提到了他们是如何优化 string 和 []byte 转换的,我便问景埕有没有问题,讨论过程中学到了很多,于是便有了这篇总结。
LA0WAN9
2021/12/14
5730
深度解密Go语言之unsafe
个人认为,学习本身并不是一件轻松愉快的事情,寓教于乐是个美好的愿望。想要深刻地领悟,就得付出别人看不见的努力。学习从来都不会是一件轻松的事情,枯燥是正常的。耐住性子,深入研究某个问题,读书、看文章、写博客都可以,浮躁时代做个专注的人!
梦醒人间
2019/06/03
6810
golang面试题:字符串转成byte数组,会发生内存拷贝吗?
字符串转成切片,会产生拷贝。严格来说,只要是发生类型强转都会发生内存拷贝。那么问题来了。 频繁的内存拷贝操作听起来对性能不大友好。有没有什么办法可以在字符串转成切片的时候不用发生拷贝呢?
9号同学
2021/03/04
1.5K0
Go语言slice的本质-SliceHeader
今天最热的事情,莫过于微信7.0的发布,增加了短视频,优化了看一看等功能,本来想跟着个热度,蹭个流量,后来发现各位大佬都已经开始蹭了,就算了,还是谈谈Go语言(golang)吧,看来要成为一个合格的自媒体,还是不要矜持,任重道远啊。
飞雪无情
2020/02/10
7710
Golang 语言怎么高效使用字符串?
在 Golang 语言中,string 类型的值是只读的,不可以被修改。如果需要修改,通常的做法是对原字符串进行截取和拼接操作,从而生成一个新字符串,但是会涉及内存分配和数据拷贝,从而有性能开销。本文我们介绍在 Golang 语言中怎么高效使用字符串。
frank.
2021/04/16
1.9K0
【Go】string 优化误区及建议
初学 Go 语言的朋友总会在传 []byte 和 string 之间有着很多纠结,实际上是没有了解 string 与 slice 的本质,而且读了一些程序源码,也发现很多与之相关的问题,下面类似的代码估计很多初学者都写过,也充分说明了作者当时内心的纠结:
thinkeridea
2019/11/04
9520
【Go】string 优化误区及建议
Go通关08:断言、反射的理解与使用!
您诸位好啊,我是无尘,学习Go语言肯定经常看到断言、反射这两个词,曾因为使用场景不太熟悉,让我很是费解,今天就好好唠唠!
微客鸟窝
2021/08/18
1.1K0
[]byte与string的两种转换方式和底层实现
对小许公众号点了关注的朋友,应该都看过小许之前的文章《fasthttp是如何做到比net/http快十倍的》,相信你们还对极致的优化方式意犹未尽。
小许code
2024/03/25
3771
[]byte与string的两种转换方式和底层实现
【Go】深入剖析slice和array
array 和 slice 看似相似,却有着极大的不同,但他们之间还有着千次万缕的联系 slice 是引用类型、是 array 的引用,相当于动态数组, 这些都是 slice 的特性,但是 slice 底层如何表现,内存中是如何分配的,特别是在程序中大量使用 slice 的情况下,怎样可以高效使用 slice? 今天借助 Go 的 unsafe 包来探索 array 和 slice 的各种奥妙。
thinkeridea
2019/11/04
4810
【Go】深入剖析slice和array
Go看源码必会知识之unsafe包
众所周知,Go语言被设计成一门强类型的静态语言,那么他的类型就不能改变了,静态也是意味着类型检查在运行前就做了。所以在Go语言中是不允许两个指针类型进行转换的,使用过C语言的朋友应该知道这在C语言中是可以实现的,Go中不允许这么使用是处于安全考虑,毕竟强制转型会引起各种各样的麻烦,有时这些麻烦很容易被察觉,有时他们却又隐藏极深,难以察觉。大多数读者可能不明白为什么类型转换是不安全的,这里用C语言举一个简单的例子:
Golang梦工厂
2022/07/08
2850
Go看源码必会知识之unsafe包
Golang 语言中数组和切片的区别是什么?
在很多编程语言中都有数组,而切片类型却不常见。实际上,Golang 语言中的切片的底层存储也是基于数组。因为数组是固定长度的,而切片比数组更加灵活,所以在 Golang 语言中,数组使用的并不多,切片使用更加广泛。
frank.
2021/04/16
5230
推荐阅读
相关推荐
Go语言的引用类型
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档