前言
golang的slice切片大家应该都用过,如果说他就是个数组,那是不准确的。在这里我会和大家一起探讨golang中切片的一些逻辑。
slice组成
如图:
其中:
data包含真实的地址和字节长度,你可以理解为就是我们传统意义上理解的数组连续内存;
len表示实际的切片内元素数量;
cap表示切片的真实底层数量;
cap不能小于len。
- 问:什么情况下cap大于len呢,什么情况下又等于?
通过几个初始化代码来解析:
// 生成一个容量是10的切片数组,这里cap==len==10
make([]int,10)
// 生成一个容量是10的切片数组,但是cap==20 len==10 cap>len
make([]int,10,20)
其实这个cap是程序预先为我们的这个切片对象申请的空间;
而len是我们这个切片实际能使用的空间;
cap存在的意义
首先我们要明确一点,数组一定是在内存中是连续的一段内存块。
对于我们golang中的slice,其实是有封装一个元素递增的功能也就是Append;
Append方法可以使我们很方便的将新元素放到我们的slice里。
-
但是使用他之余,我们不禁会想,slice中的data实际上也是个数组啊,数组怎么能像链表一样随意递增呢?
其实所谓append的核心原理也是相当于为slice中的data重新申请一块比之前更大的连续的内存段,然后将原data的数据迁移到新申请的内存段中。
这个应该是我们在写c、c++的时候数组扩容常用的方式,也是golang数组扩容用到的,但是除此外slice切片就没什么特点了么?; -
cap闪亮登场;
我们举个例子:
/* 通过上文讲解,可以理解这样做相当于为我们的slice中的data预申请
了100空间大小的内存,只不过当前可用的大小是10*/
arr:=make([]int,10,100)
那么如果我们已经往arr切片中塞入了10个元素后,当我们继续往arr切片塞数据的时候,此时并不会触发重新申请更大内存空间的动作,因为毕竟预先申请了100的连续内存空间,就继续在原来的内存段上扩容即可。
- 好处
通过预先申请空间这样子的逻辑操作,避免了因为频繁新增数据,导致程序对内存的频繁访问,以及数据频繁的迁移。节省开支;
slice扩容规则
上文讲解了slice的cap存在的意义,预先申请一大段内存段上,规避了因为频繁的增加数组元素导致的程序对内存的频繁操作。但是cap毕竟也有用完的时候,当cap等于len的时候(实际元素数量等于slice预先申请的空间数量)就会触发slice扩容;
slice扩容简言之和上文的数组扩容一致,就是找个更大的内存段拿来用,然后将原data数据迁移到新的内存段上;
- 每次slice扩容,是该扩多少才合理呢