今天废话不用多说,咱们来直接进入正题

切片究竟是什么?

在聊切片之前,我们先来看一下golang中的数组,大家都知道golang其实是c语言写的,那么在数组这一块golang和c语言的含义一样么?当然是不一样的。

golang数组

  • Go数组是值语义的,这意味着一个数组变量表示的是「整个数组」。
  • Go语言中传递数组是纯粹的「值拷贝」。

c语言数组

  • 数组变量可视为指向数组「第一个元素的指针」。

因为golang中数组是纯粹的值拷贝,所以在golang中,更地道的方式是使用「切片」, 「切片之于数组就像是文件描述符之于文件」数组更多是“退居幕后”,承担的是底层存储空间的角色;而切片则走向“前台”,为底层的存储(数组)打开了一个访问的“窗口”。

切片和数组的关系

其实通过golang源码也可以看出来,其实切片就是数组的指针。

如何声明一个切片?

方式一

我们看到通过上述语句创建的切片,编译器会自动为切片建立一个「底层数组」,如果没有在make中指定cap参数,那么cap = len,即编译器建立的数组长度为len。

方式二(数组切片化)

数组切片化

  • 切片s打开了一个操作数组u的窗口。
  • 切片截取数组是「左包含右不包含」的原则。比如u[3,7]为包含u[3]但是不包含u[7]。
  • 「切片的长度len」为4,计算方式为(high-low),在这个case中也就是7-3=4。
  • 「切片的容量cap」为s的第一个元素s[0]到数组u的末尾,所以是7。

当然可以基于一个数组建立多个切片

基于一个数组建立多个切片

也可以基于已有切片再次创建切片,也叫reslicing

reslicing

动态扩容

在讲动态扩容之前,我们先来看一些例子。

我们看到切片s的len值是线性增长的,但cap值却呈现出不规则的变化。通过下图我们更容易看清楚多次append操作究竟是如何让切片进行动态扩容的。

动态扩容

我们看到append会根据切片的需要,在「当前底层数组容量无法满足」的情况下,「动态分配新的数组」,新数组长度会按一定算法扩展(参见$GOROOT/src/runtime/slice.go中的growslice函数)。新数组建立后,append会把「旧数组中的数据复制到新数组中」,之后新数组便成为切片的底层数组,旧数组后续会被「垃圾回收」掉。

这样的append操作有时会给Gopher带来一些困惑,比如通过语法u[low: high]形式进行数组切片化而创建的切片,一旦切片cap触碰到数组的上界,再对切片进行append操作,切片就会和原数组解除绑定。

小结练习

根据自己对切片的理解,先看看自己能不能想到每一步结果都会输出啥。

答案揭晓

我们看到在添加元素25之后,切片的元素已经触碰到底层数组u的边界;此后再添加元素26,append发现底层数组已经无法满足添加新元素的要求,于是新创建了一个底层数组(数组长度为cap(s)的2倍,即8),并将原切片的元素复制到新数组中。在这之后,即便再修改切片中的元素值,原数组u的元素也没有发生任何改变,因为此时切片s与数组u已经解除了绑定关系,s已经不再是数组u的描述符了。