前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >爆文推荐| Go slice append 之后的微妙变化

爆文推荐| Go slice append 之后的微妙变化

作者头像
haohongfan
发布2021-09-15 12:34:25
9960
发布2021-09-15 12:34:25
举报
文章被收录于专栏:HHFCodeRv

HHF 注:相信大家对于 Go slice 的底层数组扩容的原理比较了解了,也比较敏感。但是下面这道题的原理你是否能想明白呢?

原文的评论区更加精彩,如果看完有不明白的,可以去原文的评论区查看。



前几天听到咱 Go 读者交流群里的小伙伴私聊我,表示他们在群里一直在讨论一个问题 slice 相关的问题,众说纷纭,争议了好久,我看消息都是晚上 6 点多的了。

来自煎鱼的聊天记录

今天和各位小伙伴们一起来研究一下,避免后续又踩一遍坑,共同进步!

问题代码

引起群内大范围讨论的代码如下:

代码语言:javascript
复制
func main() {
 sl := make([]int, 0, 10)
 var appenFunc = func(s []int) {
  s = append(s, 10, 20, 30)
  fmt.Println(s)
 }
 fmt.Println(sl)
 appenFunc(sl)
 fmt.Println(sl)
 fmt.Println(sl[:10])
}

你认为程序的输出结果是什么?

是如下的答案:

代码语言:javascript
复制
[]
[10 20 30]
[]
[]

对吗?

看上去很有道理,但错了。正确的结果是:

代码语言:javascript
复制
[]
[10 20 30]
[]
[10 20 30 0 0 0 0 0 0 0]

这下可把大家整懵了,为什么输出 slsl[:10] 的结果差别这么大,这与预期的输出结果不一致。

群内小伙伴的问题更明确了,疑惑点是:

代码语言:javascript
复制
 fmt.Println(sl)     
 fmt.Println(sl[:10]) 

上述代码中,为什么第一个 sl 打印结果是空的,第二个 sl 给索引位置就能打印出来

也有小伙伴不断在尝试 sl[:10] 以外的输出,有没有因为一些边界值改变而导致不行。

例如:

代码语言:javascript
复制
fmt.Println(sl[:])

你认为这个对应的输出结果是什么?

是如下的答案:

代码语言:javascript
复制
[10 20 30 0 0 0 0 0 0 0]

对吗?

看上去很有道理,但错了。正确的结果是:

代码语言:javascript
复制
[]

是没有任何元素输出,这下大家更懵了。为什么 sl[:] 的输出结果为空?

再看看变量 sl 的长度和容量:

代码语言:javascript
复制
fmt.Println(len(sl), cap(sl))

输出结果:

代码语言:javascript
复制
0 10

长度竟然是 0 ...迷了?

挖掘原因

三个问题

在研究了问题代码的表象后,我们要进一步的挖掘问题的原因。

请思考如下三个问题:

  1. 为什么打印 sl[:10] 时,结果包含了 10 个元素,还包含了函数闭包中插入的 10, 20, 30,之间有什么关系?
  2. 为什么打印 sl 变量时,结果为空?
  3. 为什么打印 sl[:] 时,结果为空。但打印 sl[:10] 就正常输出?

了解底层

要分析起源,我们就必须要再提到 slice(切片)的底层实现,slice 底层存储的数据结构指向了一个 array(数组)。

如下:

slice 和 array 的友谊小船

对应的 Slice 在运行时的表现是 SliceHeader 结构体,定义如下:

代码语言:javascript
复制
type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
}
  • Data:指向具体的底层数组。
  • Len:代表切片的长度。
  • Cap:代表切片的容量。

核心要记住的是:slice 真正存储数据的地方,是一个数组。slice 的结构中存储的是指向所引用的数组指针地址

分析原因

在了解 slice 的底层后,我们需要来分析问题的起源,也就是那段 Go 程序。

我们关注到 appenFunc 变量,他其实是一个函数,并且结果中我们所看到的 10, 20, 30,也只有这里有插入的动作。因此这是需要分析的。

如下:

代码语言:javascript
复制
func main() {
 sl := make([]int, 0, 10)
 var appenFunc = func(s []int) {
  s = append(s, 10, 20, 30)
 }
 appenFunc(sl)
 fmt.Println(sl)
 fmt.Println(sl[:10])
}

但为什么在 appenFunc 函数中所插入的 10, 20, 30 元素,就跑到外面的切片 sl 中去了呢?

这其实结合 slice 的底层设计和函数传递就明白了,在 Go 语言中,只有值传递

具体可详见我之前写的《又吵起来了,Go 是传值还是传引用?》,有明确分析和说明。

实质上在调用 appenFunc(sl) 函数时,实际上修改了底层所指向的数组,自然也就会发生变化,也就不难理解为什么 10, 20, 30 元素会出现了。

那为什么 sl 变量的长度是 0,甚至有人猜测是不是扩容了,这其实和上面的问题还是一样,因为是值传递,自然也就不会发生变化。

要记住一个关键点:如果传过去的值是指向内存空间的地址,是可以对这块内存空间做修改的。反之,你也改不了。

至此,也就解决了我们的第一个大问题。

切片小优化

还剩下两个大问题,这似乎用上面的结论没法完整解释。虽说程序是诱因,但这块最直接的影响是和切片访问的小优化有关。

常用的访问切片我们会用:

代码语言:javascript
复制
s[low : high]

注意这里是:low、high。可没有用 len、cap 这种定性的词语,也就代表着这里取的值是可变的。

当是切片(slice)时,表达式 s[low : high] 中的 high,最大的取值范围对应着切片的容量(cap),不是单纯的长度(len)。因此调用 fmt.Println(sl[:10]) 时可以输出容量范围内的值,不会出现越界。

相对的 fmt.Println(sl) 因为该切片 len 值为 0,没有指定最大索引值,high 则取 len 值,导致输出结果为空。

至此,第二和第三个大问题就解决了。

注:访问元素在 Go 编译期就确定的了,相关逻辑可以在 compile 相关的代码中看到。

总结

在今天这篇文章中,我们结合了 Go 语言中切片的基本底层原理、值传递、边界值取值等进行了多轮探讨。

我们要牢记:如果传过去的值是指向内存空间的地址,是可以对这块内存空间做修改的。这在多种应用场景下都是适用的。

所谓的最大取值范围,除非官方给你写定 len 或 cap,否则不要过于主观的认为,因为他会根据访问的数据类型和访问定位等改变

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

本文分享自 HHFCodeRv 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 问题代码
  • 挖掘原因
    • 三个问题
      • 了解底层
        • 分析原因
          • 切片小优化
          • 总结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档