slice 结构
slice 有三个部分:元素存哪里、存了多少个元素、可以存多少个元素。 slice结构在golang源码中就是这样定义的:
1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}
创建slice
创建一个slice 可以使用以下几种方式。
直接声明
声明一个整型slice
1
var ints []int
slice 元素要存在一块连续的内存中,实际上就是个数组,这时
1
2
3
array = nil //只分配了切片结构,还没有分配底层数组,所以这里为nil
len = 0 //存储元素个数为0
cap = 0 //容量也为0
make
如果是通过make的方式定义这个变量:
1
var ints []int = make([]int, 2, 5)
此时不仅会分配切片结构,还会开辟一段内存作为它的底层数组。这里make会为ints变量开辟一段容纳5个整型元素的内存,还会把它们初始化为整型的默认值0。但是目前这个slice变量只存储了两个元素:
1
2
3
array = [0, 0, 0, 0, 0]
len = 2 //存储元素个数为2
cap = 5 //容量为5
已经存了两个,现在添加一个元素试试:
1
2
3
4
5
ints = append(ints, 1)
//此时的结构为:
//array = [0, 0, 1, 0, 0]
//len = 3 //存储元素个数为3
//cap = 5 //容量为5
已经存储的元素是可以安全读写的,但是超出这个范围就属于越界访问,引发panic:
1
2
3
4
5
ints[0] //ok
ints[1] //ok
ints[2] //ok
ints[3] //panic
ints[4] //panic
new
通过new创建slice变量,也会创建一个切片结构,但是new和make不用的是:new不会分配底层数组, new的返回值就是slice结构的起始地址。
1
2
3
4
5
s := new([]string)
//此时结构为:
//array = nil
//len = 0
//cap = 0
那谁来给它分配底层数组呢?通过append!
1
*s = append(*s, "hello")
底层数组
以上的示例中slice都是指向底层数组的首地址,但是它并不是必须指向底层数组起始地址的。
看个例子:
1
arr := [10]int{0,1,2,3,4,5,6,7,8,9}
变量arr是一个容量为10的整型数组(注意数组容量声明了就不能再变了!),再把不同的slice关联到同一个数组:
1
2
var s1 []int = arr[1:4]
var s2 []int = arr[7:]
s1 和 s2 两个slice 会共用底层数组, 看一下s1 和 s2的具体结构就明白了:
1
2
3
4
5
6
7
8
9
s1结构:
//array 起始地址指向arr[1]
//len = 3
//cap = 9
s2结构:
//array 起始地址指向arr[7]
//len = 3
//cap = 3
如果对 s1 进行修改或append :
1
2
3
4
5
6
7
8
s1[0] = 99
s1 = append(s1, 1)
println(s1[0]) //99
println(arr[1]) //99
println(s1[3]) //1
println(arr[4]) //1
可见通过操作 s1 修改底层数组 arr。
如果对s2进行append的话,原来的底层数组就不能用了,得开辟一个新的底层数组,原来的元素要拷贝过来,还要添加新元素,元素个数就变成了4, 容量扩充到了6。
1
2
3
4
5
6
s2 = append(s2, 1)
//s2结构
//array 指向新开辟的底层数组
//len = 4
//cap = 6
这下slice和底层数组的关系清晰了吧,不过还有个问题,我们只给s2添加了一个元素,容量怎么从3扩充到6了呢?那就要看slice的扩容规则了。
扩容规则
我们来看下golang中的源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
newcap := old.cap
doublecap := newcap + newcap
//如果扩容前容量翻倍还是小于所需最小容量
//那么预估容量就等于最小所需容量, 否则就在再细分
if cap > doublecap {
newcap = cap
} else {
if old.cap < 1024 {
newcap = doublecap
} else {
////如果扩容前元素个数大于等于1024,那就先扩充四分之一
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
1.预估容量
第一步是预估扩容后的容量,怎么预估?通过上面的源码可以看出来: 如果扩容前容量翻倍还是小于所需最小容量, 那么预估容量就等于最小所需容量, 否则就再细分。
如果扩容前元素个数小于1024,那就直接翻倍。 如果扩容前元素个数大于等于1024,那就先扩充四分之一,也就是扩充到原来的1.25倍。
2.预估内存
第二步预估内存。预估容量只是预估的元素个数,这么多元素要占用多少内存呢?用预估容量乘以元素类型大小得到的就是所需内存。难道直接分配这么多内存?并不是。
3.匹配合适内存
简单来说,在大多编程语言中,申请内存并不是直接和操作系统交涉,而是和语言自身实现的内存管理模块有关。
编程语言的内存管理模块会提前向操作系统申请一批内存,分成常用的规格管理起来:8字节、16、32、48、64、80、96、112…
申请内存时,内存管理模块会帮我们匹配到足够大且最接近的规格。这就是第三步要做的事情,将预估申请内存匹配到合适的内存规格。
可见slice的扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍,还有内存对齐的操作。扩容后的容量要大于或等于 原容量的 2 倍或 1.25 倍。
示例
1
2
ints := []int{1, 2}
ints = append(ints, 3, 4, 5)
看下这个例子,ints 有两个元素,append 了三个元素。
第一步先预估容量:那么所需容量至少需要扩充到5个元素。
第二步预估内存:通过预估容量x元素类型占用内存=预估内存 可算出, 在64位下:5 * 8 = 40。也就是至少要用40个字节来存储扩容后的底层数组。
第三步匹配合适内存:而实际申请时,会匹配到48字节的内存,在这个例子中每个元素占8字节, 一共能装6个元素。这就拿到扩容后的容量了。
源码
关于golang实现slice源码实现可以查看这里 https://github.com/golang/go/blob/master/src/runtime/slice.go