今天最热的事情,莫过于微信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,来看看A
和A1
内部具体的字段值,这样来判断他们是否一致,我们修改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
类型后,我们分别输出他们的Data
、Len
、Cap
字段,现在我们看看输出的结果。
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
或者网站http://www.flysnow.org/,第一时间看后续精彩文章。觉得好的话,请猛击文章右下角「好看」,感谢支持。
有疑问加站长微信联系(非本文作者)