Slice的底层分析
切片又名动态数组,通过动态长度和容量实现动态数组的功能,如下图所示
下面我们可以学习一下切片各种操作的实现流程
结构体
源码位置:cmd/compile/internal/types.Slice
type slice struct{
array unsafe.Pointer //底层数组[0]的地址
len int //长度
cap int //容量
}
array:指向底层数组;
len:底层数组里存储的原始个数,访问时,不能超过slice的长度,超过访问会发生panic;
cap:底层数组的容量,make创建切片时,cap>=len。
注意:slice[a:b]:取值的范围是左闭右开
创建切片
| 序号 | 方式 | 代码示例 |
|---|---|---|
| 1 | 直接声明 | var slice [ ]int |
| 2 | 字面量 | slice :=[ ] int {1,2,3,4,5} |
| 3 | make | slice := make([ ] int, 5,10 ) |
| 4 | 切片或者数组“截取” | slice := array[1:5] 或 slice= : sourceSlice[1:5] |
-
直接声明:创建出来的slice为nil,与nil相比为ture,与空切片不一样,虽然长度和容量都为0,但是所有的空切片的arry都指向同一个地址,空切片跟nil比较为false
-
字面量:可以用索引号直接赋值,
未标注的地方为默认零值
-
make函数需要传入三个参数:切片类型,长度,容量。当然,容量可以不传,默认和长度相等。
-
截取: 截取也是比较常见的一种创建 slice 的方法,可以从数组或者 slice 直接截取,当然需要指定起止索引位置
除了表上表示的方式还有
slice := data [ 2:4:6 ] // data[low, high, max]其中data可以是数组也可以是切片,low为闭区间,high和max为开区间,max,high必须在老数组或者切片的cap内
追加和扩容切片
当往切片里面追加元素的时候,我们平常会用到append关键字
append关键字,底层会通过如下方法追加元素gc.state.append
使用 append 向 slice 追加元素,实际上是往底层数组添加元素。如果添加的元素个数+切片len > 切片的cap。这时,切片会进行扩容,会创建新的切片,如下图所示(直接偷书里的图了),扩容函数runtime.growslice
扩容策略
源码点(1.22版本):runtime.nextslicecap
期望切片容量:旧切片长度+追加元素长度
- 当
期望切片容量>两倍旧切片容量:新切片容量=期望切片容量 - 当
期望切片容量<= 两倍旧切片容量,并且旧切片容量<256:新切片容量=两倍旧切片容量 - 上面都不满足,按照
旧切片容量 / 4 + 192叠加,当叠加的值>=期望切片容量时,返回叠加值
当然最终的内存大小,还需要进行内存对齐
所以在初始化切片的时候,如果能知道切片容量的大小,应该指定容量大小,从而减少扩容的次数,提高内存利用和代码性能。
内存对齐:
为什么要内存对齐:
因为cup对内存的读取不是一位一位的读取的,是以字长为单位读取的,比如32位的cpu读取一个字长的数据都是32位的,所以内存对齐可以减少cup对内存的读取次数。
go语言的内存对齐规则:
变量的存储起始地址一定是对齐保证的整数倍,变量大小是对齐保证的整数倍,最后内存的大小,要满足结构体类型对齐保证最大值的整数倍。
在编写结构体的时候,占位大的类型,写在最后比写在前面的,结构体的内存要小。
总结
-
切片是对底层数组的一个抽象,描述了它的一个片段。
-
多个切片可能共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片。
-
append 函数会在切片容量不够的情况下,会扩容并迁移到新的内存,所以会改变原来底层数组的位置。
-
扩容策略规则计算的容量,最后还需要进行内存对齐的操作来确认具体申请内存的大小。
学习文献: Go 语言设计与实现