0%

golang的slice实现原理

slice 切片,也可以理解为动态数组。与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

1. 介绍

1.1 数据结构

1
2
3
4
5
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
  • Data 是指向数组的指针;
  • Len 是当前切片的长度;
  • Cap 是当前切片的容量,即 Data 数组的大小:
golang-slice-struct

1.2 初始化

1
2
3
4
5
slice := make([]int, 10) // 内存空间 = 切片中元素大小 × 切片容量

slice := []int{1, 2, 3}

arr[0:3]

使用下标初始化切片不会拷贝原数组或者原切片中的数据,它只会创建一个指向原数组的切片结构体,所以修改新切片的数据也会修改原切片。

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
arr1 := []int{1, 2, 3, 4, 5}
arr2 := arr1[1:3]
fmt.Println(arr1, arr2)
arr2[0] = 100
fmt.Println(arr1, arr2) // 同时修改了
}

/*
[1 2 3 4 5] [2 3]
[1 100 3 4 5] [100 3]
*/

2. 引用机制

2.1 不同初始化方式

1
2
3
slice := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
s1 := slice[2:5] // 2,3,4
s2 := s1[2:6:7] // data[low, high, max] 4,5,6,7
slice origin

2.2 修改引用

注意,向 s2 尾部追加一个元素 100

1
s2 = append(s2, 100)

s2 容量刚好够,直接追加。不过,这会修改原始数组对应位置的元素。这一改动,数组和 s1 都可以看得到。

append 100

2.3 扩容拷贝

再次向 s2 追加元素200:

1
s2 = append(s2, 200)

这时,s2 的容量不够用,该扩容了。于是,s2 另起炉灶,将原来的元素复制新的位置,扩大自己的容量。

并且为了应对未来可能的 append 带来的再一次扩容,s2 会在此次扩容的时候多留一些 buffer,将新的容量将扩大为原始容量的2倍,也就是10了。

append 200

4.4 修改其他引用

最后,修改 s1 索引为2位置的元素:

1
s1[2] = 20

这次只会影响原始数组相应位置的元素。它影响不到 s2 了,人家已经远走高飞了。

s1[2]=20

再提一点,打印 s1 的时候,只会打印出 s1 长度以内的元素。所以,只会打印出3个元素,虽然它的底层数组不止3个元素。

3. 扩容机制

3.1 源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func growslice(et *_type, old slice, cap int) slice {
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
  • 如果期望容量大于当前容量的两倍就会使用期望容量;

  • 如果当前切片的长度小于 1024 就会将容量翻倍;

  • 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

3.2 cap不足重新分配

下面这个例子底层用的同一个数组, cap充足没有重新分配新数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
m := make([]int, 3, 4)
a := append(m, 1)
b := append(m, 2)
fmt.Printf("m: %v p:%p\n", m, m) // m: [0 0 0] p:0xc00008a000
fmt.Printf("a: %v p:%p\n", a, a) // a: [0 0 0 2] p:0xc00008a000
fmt.Printf("b: %v p:%p\n", b, b) // b: [0 0 0 2] p:0xc00008a000

a[0] = 10
b[0] = 100
fmt.Printf("m: %v p:%p\n", m, m) // m: [100 0 0] p:0xc00008a000
fmt.Printf("a: %v p:%p\n", a, a) // a: [100 0 0 2] p:0xc00008a000
fmt.Printf("b: %v p:%p\n", b, b) // b: [100 0 0 2] p:0xc00008a000
}

下面这个例子, 底层用了两个数组, b扩容的时候, cap不足另起炉灶了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
m := make([]int, 3, 4)
a := append(m, 1)
b := append(a, 2)
fmt.Printf("m: %v p:%p\n", m, m) // m: [0 0 0] p:0xc000018220
fmt.Printf("a: %v p:%p\n", a, a) // a: [0 0 0 1] p:0xc000018220
fmt.Printf("b: %v p:%p\n", b, b) // b: [0 0 0 1 2] p:0xc00001c180

a[0] = 10
b[0] = 100
fmt.Printf("m: %v p:%p\n", m, m) // m: [10 0 0] p:0xc000018220
fmt.Printf("a: %v p:%p\n", a, a) // a: [10 0 0 1] p:0xc000018220
fmt.Printf("b: %v p:%p\n", b, b) // b: [100 0 0 1 2] p:0xc00001c180
}

3.3 扩容时底层ptr指向新的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
func main() {
ss := []int{}
for i := 1; i < 20; i++ {
fmt.Printf("addr:%p %p,len:%d,cap:%d\n", ss, &ss, len(ss), cap(ss))
ss = append(ss, i)
}
}

/*
addr:0x11aac78 0xc0000a6020,len:0,cap:0

addr:0xc0000b4020 0xc0000a6020,len:1,cap:1

addr:0xc0000b4040 0xc0000a6020,len:2,cap:2

addr:0xc0000b6020 0xc0000a6020,len:3,cap:4
addr:0xc0000b6020 0xc0000a6020,len:4,cap:4

addr:0xc0000b8040 0xc0000a6020,len:5,cap:8
addr:0xc0000b8040 0xc0000a6020,len:6,cap:8
addr:0xc0000b8040 0xc0000a6020,len:7,cap:8
addr:0xc0000b8040 0xc0000a6020,len:8,cap:8

addr:0xc0000ba000 0xc0000a6020,len:9,cap:16
addr:0xc0000ba000 0xc0000a6020,len:10,cap:16
addr:0xc0000ba000 0xc0000a6020,len:11,cap:16
addr:0xc0000ba000 0xc0000a6020,len:12,cap:16
addr:0xc0000ba000 0xc0000a6020,len:13,cap:16
addr:0xc0000ba000 0xc0000a6020,len:14,cap:16
addr:0xc0000ba000 0xc0000a6020,len:15,cap:16
addr:0xc0000ba000 0xc0000a6020,len:16,cap:16

addr:0xc0000bc000 0xc0000a6020,len:17,cap:32
addr:0xc0000bc000 0xc0000a6020,len:18,cap:32

*/
  • 可以看出, cap 是2倍增长的(<1024)

  • 当cap变化的时候,ss 的指针变了。

  • &ss 的指针没有变,因为 &ss操作返回的是该切片的内部指针地址。

4. 头脑风暴

  • cap小于1024的时候是2倍增长的,超过1024每次增加25%
  • slice := make([]int, 0, 10),第三个参数是cap。

5. 参考资料

给作者打赏,可以加首页微信,咨询作者相关问题!