Loading... > 原视频及文案出自幼麟实验室 <iframe class="iframe_video" src="//player.bilibili.com/player.html?aid=413046944&bvid=BV1CV411d7W8&cid=205107628&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe> ## slice类型基本结构 slice由三个部分组成: `data`:元素存哪里 `len`:存了多少个元素 `cap`:可以存多少个元素 举个例子,声明一个整型slice: ```go var ints []int ``` 变量ints的结构如下图所示: <img src="https://bu.dusays.com/2022/04/12/6da76a1383df8.png" alt="slice2 (1)" style=""> slice的元素要存在一段连续的内存中,实际上就是个数组 ,data就是这个底层数组的起始地址。但目前只分配了这个切片结构,还没有分配底层数组,所以data为nil,存储元素个数len为0,容量cap也为0。 如果通过make的方式定义这个变量,不仅会分配这三部分结构,还会开辟一段内存作为它的底层数组。 ```go var ints []int = make([]int, 2, 5) ``` 这里make会为ints开辟一段容纳5个整型元素的内存,还会把它们初始化为整型的默认值 0。但是目前这个slice变量只存储了两个元素,所以变量ints的结构如下图所示。  接下来,添个元素试试~ ```go ints = append(ints, 1) ``` 已经存了两个,所以新添加的是第三个,len修改为3。  ```go ints[0] = 1 ``` 已经存储的元素是可以安全读写的,但是超出这个范围就属于**越界访问,**会发生panic。  再来个例子,这次我们看看字符串类型的slice,但是不用make,来试试new。 ```go ps := new([]string) ``` new一个slice变量同样会分配这三部分结构,但它不负责底层数组的分配,所以data=nil,len和cap都是0。new的返回值就是slice结构的起始地址,所以ps它就是个地址。  此时这个slice变量还没有底层数组,像下面这样的操作是不允许的: ```go (*ps)[0] = "eggo" ``` 那谁来给它分配底层数组呢? 答案是:append ```go *ps = append(*ps, "eggo") ``` 通过append的方式添加元素,append就会给它开辟底层数组。如下图所示,这里开辟了一个字符串元素的数组。  注意其中字符串类型由两部分组成,一个内容起始地址,指向字符串内容,还有一个字节长度。 接下来我们看看和slice密切相关的——底层数组。 ## 底层数组 数组,就是同种类型的元素一个挨一个的存储。[]int底层就是int数组,[]string底层就是string数组。但是slice结构中的data,并不是必须指向数组的开头。来看个例子: ```go arr := [10]int{0,1,2,3,4,5,6,7,8,9} ``` 变量arr是容量为10的整型数组(注意数组容量声明了就不能变了)。我们可以把不同的slice关联到同一个数组,如下代码所示: ```go var s1 []int = arr[1:4]var s2 []int = arr[7:] ``` s1和s2会共用底层数组arr,s1和s2的具体结构如下图所示:  s1的元素是arr索引1到4 左闭右开,所以1,2,3这3个元素算是添加到s1中了。但是容量却是从s1的data这里开始,到底层数组结束共有9个元素。 slice访问和修改的都是底层数组的元素,所以**s1[3]**就算访问越界了。 如果修改s1的定义为: ```go var s1 []int = arr[1:5] ``` 或者通过append添加元素来扩大可读写的区间范围: ```go s1 = append(s1,4) ``` 这样就可以访问s1[3]了。 再来看s2,s2的元素从索引7开始直到结束,共3个元素,容量也是3。此时 如果再给s2添加元素会怎样? ```go s2 = append(s2, 10) ```  arr这个底层数组是不能用了,得开辟新数组。但是原来的元素要拷过来,还要添加新元素10。元素个数改为4,容量扩到6。这下slice和底层数组的关系都清晰了吧! 不过,还有个问题,我只添加了一个元素,s2怎么从3扩容到6了呢?那就要看slice的扩容规则了~ ## 扩容规则 **扩容规则第一步:预估扩容后的容量。** 怎么预估?来看个例子。 ```go ints := []int{1,2} ints = append(ints, 3, 4, 5) ``` 这里扩容前容量oldCap为2,添加三个元素,那至少得扩容到cap=5吧?难道就预估到5,这么简单粗暴的吗? 当然不是,预估也是有规则的~ oldCap:扩容前容量 oldLen:扩容前元素个数 cap:扩容所需最小容量 newCap:预估容量 **Go1.15中,预估容量规则如下:** <img src="https://bu.dusays.com/2022/04/12/fe377294b6a39.jpg" alt="1649766829002" style="zoom:67%;" style=""> (1)如果扩容前的容量翻倍之后还是小于所需最小容量,那么预估容量就等于所需最小容量。 (2)如果不满足第一条,而且扩容前容量小于1024,那就直接翻倍没商量。 (3)如果不满足第一条,而且扩容前容量大于等于1024,那就循环扩容四分之一,直到大于等于所需最小容量。 在上面这个例子中,扩容前容量为2,就算翻倍了还是小于5,所以预估容量就是5。 **Go1.16中有了些变化**,和1024比较的不再是oldLen,而是oldCap。如下代码所示: ```go // 1.16 newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.cap < 1024 { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { newcap += newcap / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } } ``` 到了Go1.18时,又改成不和1024比较了,而是和256比较;并且扩容的增量也有所变化,不再是每次扩容1/4,如下代码所示: ```go //1.18 newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { const threshold = 256 if old.cap < threshold { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { // Transition from growing 2x for small slices // to growing 1.25x for large slices. This formula // gives a smooth-ish transition between the two. newcap += (newcap + 3*threshold) / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap <= 0 { newcap = cap } } } ``` **扩容规则第二步:** 预估容量只是预估的元素“个数”,这么多元素需要占用多少内存呢?这就和元素类型挂钩了。 用预估的容量,乘以元素类型大小,得到的就是所需内存。 难道直接分配这么多内存就ok了?并不是。 <img src="https://bu.dusays.com/2022/04/12/a3c7f88e58d2c.png" alt="QQ截图20220412183436" style="zoom: 50%;" style=""> 简单来说,是因为在许多编程语言中,申请分配内存并不是直接与操作系统交涉,而是和语言自身实现的内存管理模块。它会提前向操作系统申请一批内存 分成常用的规格管理起来,我们申请内存时,它会帮我们匹配到足够大、且最接近的规格。 **这就是第三步要做的事情:将预估申请内存匹配到合适的内存规格。** 在我们的例子中,预估容量为5,64位下就需要申请40字节存放扩容后的底层数组,而实际申请会匹配到48字节。 **那这么大的内存能装多少个元素呢?** 这个例子每个元素(int)占8字节,一共能装6个,这就是扩容后的容量了。 趁热打铁,再来个例子练练手~ **小练习** ```go a := []string{"My", "name", "is"} a = append(a, "eggo") ``` a是string类型的slice,64位下每个元素占16字节,以Go1.16为例。 **第一步:** 扩容前容量是3,添加一个元素,最少要扩容到4。 原容量翻倍为6,大于4; 且原容量和1024比,小于1024,所以直接翻倍,预估容量为6。 **第二步:** 预估容量乘以元素大小(6*16=96),等于96字节。 **第三步:** 96字节匹配到内存规格也是96字节。 所以,最终扩容后容量为6。 最后修改:2022 年 04 月 21 日 © 允许规范转载 打赏 赞赏作者 支付宝微信 赞 1 如果觉得我的文章对你有用,请随意赞赏