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 语言设计与实现