likes
comments
collection
share

Golang slice原理浅析

作者站长头像
站长
· 阅读数 36

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:容量。

Golang slice原理浅析

slice的初始化

举一个例子:

声明一个整型的slice:

var ints []int

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

Golang slice原理浅析

通过make来定义

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

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

Golang slice原理浅析

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

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

append的元素会被自动添加到第3个位置。

Golang slice原理浅析

已经存储的元素是可以安全读写的,但是超出这个范围就属于越界访问。会发生panic

再来举一个例子:

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

ps := new([]string)

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

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

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

Golang 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:]

Golang slice原理浅析

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

Golang slice原理浅析

slice的扩容规则

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

STEP1 预估扩容后的容量 newCap

预估规则:

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

Golang slice原理浅析

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。

转载自:https://juejin.cn/post/7137852552674017287
评论
请登录