Golang slice原理浅析
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。
转载自:https://juejin.cn/post/7137852552674017287