golang slice原理浅析

slice内存模型


我的go源码版本是:go1.17.2

slice的源码在Go_SDK\go1.17.2\src\runtime\slice.go中。
和map.go在同一个目录下。


首先我们来看一下slice的结构:


type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

slice的结构非常简单,只有三个部分:

  • array: 指向数组的指针。
  • len:当前长度。
  • cap:容量。

在这里插入图片描述


slice的初始化


举一个例子:

声明一个整型的slice:

var ints []int

变量ints实际上就由以下三个部分组成。
slice的元素要存在一段连续的内存中,实际上就是一个数组,但是目前只分配了这个切片结构,还没有分配底层数组。所以 data = nil | len = 0 | cap = 0

在这里插入图片描述


通过make来定义

var ints []int = make([]int, 2, 5)

这时不仅会分配这三部分结构,还会开辟一段内存作为切片的底层数组,这里make会为ints开辟一段容纳5个整型元素的内存,还会把它们初始化为整型的默认值0 。
但是目前这个slice变量只存储了两个元素,所以data指向这个底层数组的首地址 | len = 2 | cap = 5

在这里插入图片描述

这个时候我们添加一个元素,然后再做一个赋值。

ints = append(ints, 1)
ints[0] = 1

append的元素会被自动添加到第3个位置。
在这里插入图片描述
已经存储的元素是可以安全读写的,但是超出这个范围就属于越界访问。会发生panic


再来举一个例子:

我们创建一个字符串类型的slice,但是不用make,而是用new

ps := new([]string)

new一个slice变量,同样会分配这三部分结构。
但他不负责底层数组的分配,所以 data = nil | len = 0 | cap = 0 。new的返回值就是slice结构的起始地址。所以ps就是一个地址。

(*ps)[0] = "egg"  //不允许

此时这个slice变量还没有底层数组。像上述的操作是不允许的。

在这里插入图片描述
但是我们可以使用append的方式为slice添加元素。它就会为slice开辟底层数组。


slice的底层数组


int型slice的底层就是int型数组,string型slice的底层就是string型数组。

但是slice中的数组指针,并不是必须指向底层数组的开头。


我们来看一个例子:

arr := [10]{0,1,2,3,4,5,6,7,8,9} //数组容量声明了就不可改变

我们可以把不同的slice关联到同一个数组。
像这样:

var s1 []int = arr[1:4]  //左闭右开
var s2 []int = arr[7:]

在这里插入图片描述
slice访问和修改的其实都是底层数组的元素。
如果要给s1添加两个元素,直接使用append即可,这个底层数组依然可以使用。
但是如果要给s2添加元素,这个底层数组就不能再使用了,因为数组的大小是固定的。
因此,得开辟一个新的数组。原来的元素得拷贝过来,还得添加新的元素。元素个数改为4,容量扩到了6。

在这里插入图片描述

slice的扩容规则


还记得上面的例子吗?我们只给s2切片添加了1个元素,为什么cap从3扩容到了6呢?那就要看slice的扩容规则了。


STEP1 预估扩容后的容量 newCap

预估规则:

  • 如果扩容前容量翻倍,还是小于所需的最小容量,那么预估容量就等于所需的最小容量。
  • 否则就要再细分:
  • 如果扩容前元素个数小于1024,那就直接翻倍。
    • 如果扩容前元素大于等于1024,那就先扩容至原来的 1/4 。

在这里插入图片描述

STEP2 newCap需要多大内存

这一步需要计算需要的内存,这就和元素类型挂钩了。用 预估容量 * 元素类型大小 得到的就是所需内存。

但是不可以直接分配这么多内存。
简单来说,再许多编程语言中,申请内存并不是直接与操作系统交涉的,而是与语言自身实现的内存管理模块进行交涉。它会提前向操作系统申请一块内存,分成常用的规格管理起来,我们申请内存时,它会帮我们匹配到足够大且最接近的规格。(按照第一步的扩容规则来)

这就是第三步要做的事情。


STEP3 匹配到合适的内存规格


比如扩容之后的cap = 5,在64位操作系统中,就需要 5 * 8 = 40 个字节的内存,来存放底层数组。

按照扩容规则,实际申请时会匹配到48字节。一共能装6个元素。


我们来看一个例子:

创建一个string类型的slice:

a := []string{"my", "name", "is"}
a := append(a, "egg")

step1: newCap = 6

step2: 6 * 16 = 96 byte (64位中string类型的大小是16byte)

step3: 匹配到内存规格就是96字节。

所以最终扩容以后,cap就是6。

posted @ 2021-11-11 16:29  Dawnlight-_-  阅读(126)  评论(0编辑  收藏  举报