切片真的是引用类型嘛
来自公众号:新世界杂货铺
注: 以下仅代表个人看法, 如果和大部分文章内容不一致,还请酌情参考
灵魂三问
Q1. 引用和指针的区别是什么?
答: 其实他们的区别我也说不清楚, 但是他们都有一个共同点, 那就是他们能够指向真实的值, 操作他们会改变真实的值
Q2. 在go中有指针了我们为什么还要有提出引用的概念?
答:很明显这个问题我答不上来, 说实话我也只是看很多文章里面这么说, 我自己没有去认真研究过
Q3. go中真的有引用这个概念嘛?
答: 这个问题或许可以解答一下Q2. 但是我也不敢作出肯定的答复. 我抱着这个问题去官方文档搜索了一通,发现并没有搜索到“引用”相关的单词, 也没有找到引用变量的定义
灵魂三问引发的思考
灵魂三问中核心就是引用,那么这个时候我就开始思考了,在go中哪些是引用类型呢。众所周知, 大部分文章里面都会提到切片是引用类型, 我曾经也一直坚信切片就是引用类型, 但是直到我遇到了下面的代码, 我开始怀疑自己的认知了
上述代码在平时的开发中出现的频率非常高,那么我的疑问就是既然切片是引用类型,那么为什么append之后还必须要赋值给它本身呢。
append的分析
假如我们不赋值给它自己,会发生什么呢?
我们来看看下面的代码
这个结果和切片是引用类型明显是不相符合的。这中间究竟发生了什么, 我们又应该怎么做呢?接下来我们让代码说话, 此时上述简单的代码已经没有更多的信息了, 那我们尝试看看编译生成的汇编代码, 希望能够从汇编中找到蛛丝马迹。
摘抄其中部分关键汇编代码如下:
go的汇编是Plan9 的汇编。而我几乎是看不懂的,但幸运的是我发现了其中的部分关键词。
(test.go:6) CALL runtime.makeslice(SB)
和a := make([]int64, 0)
, (test.go:7) CALL runtime.growslice(SB)
和_ = append(a, 10)
都能够对应起来。 最后,我在go源码的runtime包中的slice.go
文件中发现了这两个函数, 并发现了下面这个结构体
其中调用append函数时, 汇编代码中也调用了runtime.growslice
, growslice函数签名如下
我们注意看下面的debug调用栈(debug runtime的小技巧后面有机会再介绍):
由上图知, 在本列中,append在runtime层调用了growslice
并返回了一个新的slice结构体, 而我没有将返回的变量赋值给原有变量a
, 所以导致打印的结果为空。 到这里感觉问题已经基本有点头绪了, 因为返回的是一个结构体, 所以必须要进行赋值, 否则原结构体变量是不会发生改变的。
那么新的问题来了, 如果我们append之后不赋值给原先的切片, 原先的切片是不是就没有任何变化呢?
从最开始的代码输出结果来看, 原先的切片确实没有任何变化, 但真实结果还是让我们用代码来说话
通过上面的代码知道, append函数操作后,底层的数组实际已经发生了变化, 只是因为append后的结果未赋值给变量a, 所以结构体中的len未发生变化, 导致无法正确打印切片的内容
结论
go中切片其实是一个runtime中的
slice
结构体append 操作slice底层的数组后, 改变了数组的长度或者容量, 所以需要重新赋值给原先的切片, 如果切片发生扩容原先的数组地址也要发生变化
根据以上我擅自得出结论: go中不存在引用的概念, 主要是指针, 所以go中也只有值传递
补充
slice 是一个包含 data、cap 和 len 的私有结构体
map 是一个指向 runtime.hmap 结构体的指针
chan 是一个指向 runtime.hchan 结构体的指针
Q1: runtime.makeslice返回一个unsafe.Pointer?
这个unsafe.Pointer即为切片中数组第一个元素的地址
Q2: 有些汇编代码append 没有调用runtime.growslice?
go编译器进行了部分优化, 如果切片的容量足够, 就不需要调用runtime.growslice, 容量不足就会调用runtime.growslice进行扩容, 具体扩容逻辑请参见runtime/slice.go
注: 写本文时, 笔者所用go版本为:
go1.13.4
参考
https://draveness.me/golang/docs/part2-foundation/ch05-keyword/golang-make-and-new/
版权声明: 本文为 InfoQ 作者【新世界杂货铺】的原创文章。
原文链接:【http://xie.infoq.cn/article/0218be0efee29153526332637】。未经作者许可,禁止转载。
评论