Go语言slice的本质-SliceHeader

2,311 阅读6分钟

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

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

留言起因

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

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

问题: 此时的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的区别 这篇文章 。

然后就在前两天,我收到了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/,第一时间看后续精彩文章。觉得好的话,请猛击文章右下角「好看」,感谢支持。

扫码关注