Go语言slice的本质-SliceHeader

今天最热的事情,莫过于微信7.0的发布,增长了短视频,优化了看一看等功能,原本想跟着个热度,蹭个流量,后来发现各位大佬都已经开始蹭了,就算了,仍是谈谈Go语言(golang)吧,看来要成为一个合格的自媒体,仍是不要矜持,任重道远啊。html

前两天有朋友(Weelin)在个人公众号上留言,留言的文章是这一篇 Go语言实战笔记(五)| Go 切片 ,这是一篇讲Go语言(golang) Slice(切片)的,很早的一篇文章。这位朋友的留言不是讲本身的问题,而是针对另一位朋友(Dreamerque)的留言的说明。golang

留言原由

为了连贯说明问题,咱们先来看下2018-03-17,Dreamerque这位朋友的留言:数组

有个问题困扰: 考虑将slice这种引用类型做为自定义接受者,并绑定方法以下,bash

问题: 此时的slice空间容量足够,调用方法先后其地址并不会改变,那么为什么append后的切片内部成员不会改变? 默认拷贝的副本是slice引用,应该要能修改或者添加成员才符合预期的。。微信

type Slice []int

func (A Slice)Append(value int) {
	A = append(A, value)
}

func main() {
	mSlice := make(Slice, 10, 20)
	mSlice.Append(5)
	fmt.Println(mSlice)
}
复制代码

经过代码,相信你们也看明白了,以上就是Dreamerque的问题和困惑。我当时给Dreamerque的回答是引用的数据源不一致,让他参考个人 Go语言中new和make的区别 这篇文章 。app

而后就在前两天,我收到了Weelin的留言:函数

无情你好,我理解mslice的数据源应该是没发生变化的。因为值拷贝的缘由,Append方法先后的切片惟一有关联的就是底层指向的数组,打印结果不同就是由于原来切片过短了。这个也能够在执行完Append方法后,生成一个新的切片(长度大于5)并打印验证。测试

Weelin的留言更细,分析的更准,这时候,我才知道,原来我那个回答,有点误导Dreamerque了,可能会把我说的数据源理解成更底层的Data数组了。优化

问题分析

从以上的输出打印中,咱们的确能够看到mSlice并无任何变化,就是方法Append没有起任何做用。Dreamerque的困惑是以为Slice是引用类型,修改了指向应该也会跟着改,其实咱们知道,这个修改引用的指向是在Append方法内的,离开就不起做用了。网站

其实以上都不是根本,根本是Weelin提到的,append后的Slice已经不是原来的Slice了。这时候有的朋友可能又疑惑了,append返回的Slice的指针和原Slice的指针同样的啊,怎么会不是一个呢?咱们来测试一次,修改代码以下:

func (A Slice)Append(value int) {
	A1 := append(A, value)
	fmt.Printf("%p\n%p\n",A,A1)
}
复制代码

咱们用A1存储append方法返回的Slice,而后打印返回A1和原A的指针地址,发现的确同样。你们能够本身运行试试。其实咱们本身在make一个Slice的时候会发现,是能够有三个参数的,一个是数据、一个是长度、一个是容量,也就是说,Slice是这样的一个结构,如今该是咱们的SliceHeader登场的时候了。

SliceHeader登场

SliceHeader是Slice运行时的具体表现,它的结构定义以下:

type SliceHeader struct {
	Data uintptr
	Len  int
	Cap  int
}
复制代码

正好对应Slice的三要素,Data指向具体的底层数据源数组,Len表明长度,Cap表明容量。

既然Slice就是SliceHeader,那么咱们把Slice转化为SliceHeader,来看看AA1内部具体的字段值,这样来判断他们是否一致,咱们修改Append方法以下:

//blog:www.flysnow.org
//wechat:flysnow_org

func (A Slice)Append(value int) {
	A1 := append(A, value)

	sh:=(*reflect.SliceHeader)(unsafe.Pointer(&A))
	fmt.Printf("A Data:%d,Len:%d,Cap:%d\n",sh.Data,sh.Len,sh.Cap)

	sh1:=(*reflect.SliceHeader)(unsafe.Pointer(&A1))
	fmt.Printf("A1 Data:%d,Len:%d,Cap:%d\n",sh1.Data,sh1.Len,sh1.Cap)
}
复制代码

经过unsafe.Pointer指针进行强制类型转换,关于unsafe.Pointer的知识能够参考个人 Go语言实战笔记(二十七)| Go unsafe Pointer 这篇文章。

都转换为*reflect.SliceHeader类型后,咱们分别输出他们的DataLenCap字段,如今咱们看看输出的结果。

A  Data:824634204160,Len:10,Cap:20
A1 Data:824634204160,Len:11,Cap:20
复制代码

这下你们明白了吧,他们的Len不同,并非一个Slice,因此使用append方法并无改变原来的A,而是新生成了一个A1,即便Dreamerque这位朋友经过以下代码 A = append(A, value) 进行复制,也只是一个mSlice的拷贝A的指向被改变了,并且这个A只在Append方法内有效,mSlice自己并无改变,因此输出的mSlice不会有任何变化。

这里正确的作法是让Append返回append后的结果。其实对于内置函数append的使用,Go语言(golang)官方作了说明的,要保存返回的值。

Append returns the updated slice. It is therefore necessary to store the result of append

以上Dreamerque这位朋友的例子中,设置的Len是10,Cap是20,由于Cap足够大,因此内置函数append并无生成新的底层数组,如今咱们把Cap改成10。

type Slice []int

func (A Slice)Append(value int) {
	A1 := append(A, value)

	sh:=(*reflect.SliceHeader)(unsafe.Pointer(&A))
	fmt.Printf("A Data:%d,Len:%d,Cap:%d\n",sh.Data,sh.Len,sh.Cap)

	sh1:=(*reflect.SliceHeader)(unsafe.Pointer(&A1))
	fmt.Printf("A1 Data:%d,Len:%d,Cap:%d\n",sh1.Data,sh1.Len,sh1.Cap)
}

func main() {
	mSlice := make(Slice, 10, 10)
	mSlice.Append(5)
	fmt.Println(mSlice)
}
复制代码

运行代码咱们会发现两个Slice的Data再也不同样了。

A  Data:824633835680,Len:10,Cap:10
A1 Data:824634204160,Len:11,Cap:20
复制代码

这是由于在append的时候,发现Cap不够,生成了一个新的Data数组,用于存储新的数据,而且同时扩充了Cap容量。

小结

最终,我从新回复了Dreamerque,并对Weelin作了感谢,而后想到这类问题,能够还有很多朋友会遇到,因此写了一篇文章分析下Slice的本质,也就是SliceHeader,但愿能够帮到你们,Go语言,golang ,的确够浪,SliceHeader很溜。

本文为原创文章,转载注明出处,欢迎扫码关注公众号flysnow_org或者网站www.flysnow.org/,第一时间看后续精彩文章。以为好的话,请猛击文章右下角「好看」,感谢支持。

扫码关注
相关文章
相关标签/搜索