Go-Slice底层原理剖析

181 阅读4分钟

Slice的底层分析

切片又名动态数组,通过动态长度和容量实现动态数组的功能,如下图所示

image.png

下面我们可以学习一下切片各种操作的实现流程

结构体

源码位置: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}
3makeslice := 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

image.png

扩容策略
源码点(1.22版本):runtime.nextslicecap
期望切片容量:旧切片长度+追加元素长度

  • 期望切片容量>两倍旧切片容量:新切片容量=期望切片容量
  • 期望切片容量<= 两倍旧切片容量,并且旧切片容量<256:新切片容量=两倍旧切片容量
  • 上面都不满足,按照旧切片容量 / 4 + 192叠加,当叠加的值>=期望切片容量时,返回叠加值

当然最终的内存大小,还需要进行内存对齐

所以在初始化切片的时候,如果能知道切片容量的大小,应该指定容量大小,从而减少扩容的次数,提高内存利用和代码性能。

内存对齐:

为什么要内存对齐

因为cup对内存的读取不是一位一位的读取的,是以字长为单位读取的,比如32位的cpu读取一个字长的数据都是32位的,所以内存对齐可以减少cup对内存的读取次数。

go语言的内存对齐规则

变量的存储起始地址一定是对齐保证的整数倍,变量大小是对齐保证的整数倍,最后内存的大小,要满足结构体类型对齐保证最大值的整数倍。

在编写结构体的时候,占位大的类型,写在最后比写在前面的,结构体的内存要小。

总结

  • 切片是对底层数组的一个抽象,描述了它的一个片段。

  • 多个切片可能共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片。

  • append 函数会在切片容量不够的情况下,会扩容并迁移到新的内存,所以会改变原来底层数组的位置。

  • 扩容策略规则计算的容量,最后还需要进行内存对齐的操作来确认具体申请内存的大小。

学习文献: Go 语言设计与实现