Golang 切片(深入了解切片底层扩容机制,部分源码,测试实战+核心用法)
大家如果又不喜欢文字版的,作者有对应的视频版讲解切片,超级详细,带大家精准定位golang切片必须掌握的重点内容,欢迎大家观看!!!下方链接直通视频版
别再被Golang 切片坑了! 底层源码 + 测试实战 + 核心用法(大学生精讲计算机知识,不看会后悔系列!)_哔哩哔哩_bilibili
一、来由
先有固定数组 ➡ 后有可变切片 ➡ 切片是对数组的封装与扩展
总结:数组提供底层连续内存,切片提供动态长度+共享视图,二者分工协作,成就了Go语言高效又易用的序列容器
二、定义
切片又称动态数组,依托数组实现,可以方便地进行扩容和传递,实际使用时比数组更灵活,正因为灵活,实际使用时容易出错,避免出错的方法之一便是了解其使用原理
三、操作语法
1. 初始化
声明和初始化切片的方式主要有以下几种:
1)变量声明;
这种方式声明的切片变量与声明其他类型变量一样,变量值都为零值,对于切片来讲零值为 nil
2)字面量;
字面量初始化切片,需要知道的是空切片是指长度为空,其值并不是 nil,声明长度为 0 的切片时,推荐使用变量声明的方式获得一个 nil 切片,而不是空切片,因为 nil 切片不需要分配内存
3)使用内置函数 make();
内置函数 make()可以创建切片,切片元素均初始化为相应类型的零值
推荐使用指定长度的同时指定预估空间,可以有效地减少切片扩容时内存分配及拷贝次数
4)从切片和数组中切取。
切片可以基于数组和切片创建,需要了解的是切片与原数组或切片共享底层空间吗,修改切片会影响原数组或切片
切片表达式 [low : high] 表示的是左闭右开 [low:high) 区间,切取的长度为 hight - low。
2. 内置函数 append()用于向切片中追加元素
当切片空间不足时,append()会先创建新的大容量切片,添加元素之后再返回新切片
四、实现原理
切片依托于数组实现,底层数组对用户屏蔽,在底层数组容量不足时,可以实现自动分配并生成新的切片,接下来会根据实际使用场景来分别介绍其机制
1. 数据结构
源码包中 src/runtime/slice.go:slice 定义了 slice 的数据结构:
从数据结构上看 slice 很清晰,arrray 指针指向底层数组,len 表示切片长度,cap表示底层数组容量
2. 切片操作:使用 make() 创建slice
使用make() 创建 slice 时,可以同时指定长度和容量,创建时底层会分配一个数组,数组的长度即为容量
例如,slice := make( [ ] int,5,7) 语句创建的 slice 的结构如下图所示:
3. 使用数组创建 slice
使用数组创建 slice 使用数组创建 slice 时,slice将与原数组共用一部分内存
例如,slice := array[5:7] 语句所创建的 slice 的结构图如下
4. slice扩容
使用 append 向 slice追加元素时,如果 slice 空间不足,则会触发 slice 扩容,扩容实际上是重新分配一块更大的内存,将原 slice 的数据拷贝进新 slice,然后返回新 slice,扩容后再将数据追加进去。
例如,当向一个 capacity 为 5 且 length 也为 5 的 slice 再次追加 1 个元素时,就会发生扩容,如后图所示:
扩容操作只关心容量,会把原 slice 的数据拷贝到新 slice 中,追加数据由 append 在扩容结束后完成,由上图可见,扩容后新的 slice 长度仍然是 5 ,但是容量由 5 提升到了 10,原 slice 的数据也都拷贝到了新 slice 指向的数组中
Go 1.18之前遵循的扩容基本规则:
1. 如果原 slice 的容量小于 1024,则新 slice 的容量将扩大为原来的2倍;
2. 如果原 slice 的容量大于或等于 1024,则新的 slice 的容量将扩大为原来的 1.25 倍
优化之前的源码部分:
Go 1.18扩容机制优化版:
Go1.18不再以1024为临界点,而是设定了一个值为256的threshold,以256为临界点;超过256,不再是每次扩容1/4,而是每次增加(旧容量+3*256)/4;
1. 当新切片需要的容量cap大于两倍扩容的容量,则直接按照新切片需要的容量扩容;
2. 当原 slice 容量 < threshold 的时候,新 slice 容量变成原来的 2 倍;
3. 当原 slice 容量 > threshold,进入一个循环,每次容量增加
扩容机制优化说明:
在 Go 1.18中,优化了切片扩容的策略,让底层数组大小的增长更加平滑: 通过减小阈值并固定增加一个常数,使得优化后的扩容的系数在阈值前后不再会出现从2到1.25的突变
原始容量对应的扩容系数:
可以看到,Go1.18的扩容策略中,随着容量的增大,其扩容系数是越来越小的,可以更好地节省内存。
Go 1.18优化的扩容机制代码单元测试:
代码演示运行结果:
大家可以自己下去尝试一下!
五、切片表达式
slice 表达式可以基于一个字符串(string)生成子字符串,也可以从一个数组或切片中生成切片,Go语言提供了两种 slice 表达式:
1. 简单表达式:a [low :high]; 2. 扩展表达式:a [low : high : max] 。
1. 简单表达式,其格式为:
如果 a 为数组或切片,则该表达式将切取 a 位于 [low,high] 区间的元素并生成一个新的切片,简单表达式生成的切片的长度为 high - low ,例如下面的例子:
边界问题:

默认值:
为了使用方便,简单表达式 a[low :high] 中的 low 和 high 都是可以省略的,low的默认值为 0,而 high 的默认值为表达式作用对象的长度
2. 扩展表达式
Go 团队很早就关注到了这个风险,并且在 Go1.2 中就提供了一种可以限制新切片容量的表达式,即扩展表达式:
注意:扩展表达式中的 max 用于限制新生成切片的容量,新切片的容量为 max - low
注意:
1. 扩展表达式中的a[low : high : max] 只有 low 是可以省略的,其默认值为0,
2. 如果缺失了 high 或 max则会产生编译错误
六、切片使用技巧及错误规避:
1. 预分配容量优化性能:
2. 误用共享底层数组:
3. 切片作为参数传递:
update:共享底层数组,所以原切片也会被修改,会打印 [0, 90, 2], 底层数据结构的指针,len,cap不变
add: 函数里面的 len = 4,cap不变,但是回到main函数的 len 依旧是 3,所以main函数的切片最终只会输出三个数