本文主要分享下Go的切片:长度和容量的内容。
1. 切片的声明
切片可以看成是数组的引用。在 Go 中,每个数组的大小是固定的,不能随意改变大小,切片可以为数组提供动态增长和缩小的需求,但其本身并不存储任何数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* 这是一个数组的声明
*/
var a [ 5 ] int //只指定长度,元素初始化为默认值0
var a [ 5 ] int { 1 , 2 , 3 , 4 , 5 }
/*
* 这是一个切片的声明:即声明一个没有长度的数组
*/
// 数组未创建
// 方法1:直接初始化
var s [] int //声明一个长度和容量为 0 的 nil 切片
var s [] int { 1 , 2 , 3 , 4 , 5 } // 同时创建一个长度为5的数组
// 方法2:用make()函数来创建切片:var 变量名 = make([]变量类型,长度,容量)
var s = make ([] int , 0 , 5 )
// 数组已创建
// 切分数组:var 变量名 []变量类型 = arr[low, high],low和high为数组的索引。
var arr = [ 5 ] int { 1 , 2 , 3 , 4 , 5 }
var slice [] int = arr [ 1 : 4 ] // [2,3,4]
2. 切片的长度和容量
切片的长度是它所包含的元素个数。
切片的容量是从它的第一个元素到其底层数组元素末尾的个数。
切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。
1
2
3
s := [] int { 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 } // [0 1 2 3 4 5 6 7 8 9] len=10,cap=10
s1 := s [ 0 : 5 ] // [0 1 2 3 4] len=5,cap=10
s2 := s [ 5 :] // [5 6 7 8 9] len=5,cap=5
3. 切片追加元素后长度和容量的变化
3.1 append 函数
Go 提供了内建的 append 函数,为切片追加新的元素。
1
func append ( s [] T , vs ... T ) [] T
下面分两种情况描述了向切片追加新元素后切片长度和容量的变化。
Example 1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
func main () {
arr := [ 5 ] int { 1 , 2 , 3 , 4 , 5 } // [1 2 3 4 5]
fmt . Println ( arr )
s1 := arr [ 0 : 3 ] // [1 2 3]
printSlice ( s1 )
s1 = append ( s1 , 6 )
printSlice ( s1 )
fmt . Println ( arr )
}
func printSlice ( s [] int ) {
fmt . Printf ( "len=%d cap=%d %p %vn" , len ( s ), cap ( s ), s , s )
}
执行结果如下:
1
2
3
4
[ 1 2 3 4 5 ]
len = 3 cap = 5 0xc000082030 [ 1 2 3 ]
len = 4 cap = 5 0xc000082030 [ 1 2 3 6 ]
[ 1 2 3 6 5 ]
可以看到切片在追加元素后,其容量和指针地址没有变化,但底层数组发生了变化,下标 3 对应的 4 变成了 6。
Example 2:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
func main () {
arr := [ 5 ] int { 1 , 2 , 3 , 4 } // [1 2 3 4 0]
fmt . Println ( arr )
s2 := arr [ 2 :] // [3 4 0]
printSlice ( s2 )
s2 = append ( s2 , 5 )
printSlice ( s2 )
fmt . Println ( arr )
}
func printSlice ( s [] int ) {
fmt . Printf ( "len=%d cap=%d %p %vn" , len ( s ), cap ( s ), s , s )
}
执行结果如下:
1
2
3
4
[ 1 2 3 4 0 ]
len = 3 cap = 3 0xc00001c130 [ 3 4 0 ]
len = 4 cap = 6 0xc00001c180 [ 3 4 0 5 ]
[ 1 2 3 4 0 ]
而这个切片在追加元素后,其容量和指针地址发生了变化,但底层数组未变。
当切片的底层数组不足以容纳所有给定值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。
3.2 切片的源代码学习
Go 中切片的数据结构可以在源码下的 src/runtime/slice.go 查看。
1
2
3
4
5
6
// go 1.3.16 src/runtime/slice.go:13
type slice struct {
array unsafe . Pointer
len int
cap int
}
可以看到,切片作为数组的引用,有三个属性字段:长度、容量和指向数组的指针。
向 slice 追加元素的时候,若容量不够,会调用 growslice 函数,
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
37
38
39
// go 1.3.16 src/runtime/slice.go:76
func growslice ( et * _type , old slice , cap int ) slice {
//...code
newcap := old . cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old . len < 1024 {
newcap = doublecap
} else {
// 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
}
}
}
// 跟据切片类型和容量计算要分配内存的大小
var overflow bool
var lenmem , newlenmem , capmem uintptr
switch {
// ...code
}
// ...code...
// 将旧切片的数据搬到新切片开辟的地址中
memmove ( p , old . array , lenmem )
return slice { p , old . len , newcap }
}
从上面的源码,在对 slice 进行 append 等操作时,可能会造成 slice 的自动扩容。其扩容时的大小增长规则是:
如果切片的容量小于 1024,则扩容时其容量大小乘以2;一旦容量大小超过 1024,则增长因子变成 1.25,即每次增加原来容量的四分之一。
如果扩容之后,还没有触及原数组的容量,则切片中的指针指向的还是原数组,如果扩容后超过了原数组的容量,则开辟一块新的内存,把原来的值拷贝过来,这种情况丝毫不会影响到原数组。
上面的两个例子中,切片的容量均小于 1024 个元素,所以扩容的时候增长因子为 2,每增加一个元素,其容量翻番。
Example2 中,因为切片的底层数组没有足够的可用容量,append() 函数会创建一个新的底层数组,将被引用的现有的值复制到新数组里,再追加新的值,所以原数组没有变化,不是我想象中的[1 2 3 4 5],
3.3 切片扩容的内部实现
扩容1:切片扩容后其容量不变
1
2
3
4
5
6
7
slice := [] int { 1 , 2 , 3 , 4 , 5 }
// 创建新的切片,其长度为 2 个元素,容量为 4 个元素
mySlice := slice [ 1 : 3 ]
// 使用原有的容量来分配一个新元素,将新元素赋值为 40
mySlice = append ( mySlice , 40 )
fmt . Printf ( "len=%d cap=%d %p %v\n" , len ( slice ), cap ( slice ), slice , slice )
fmt . Printf ( "len=%d cap=%d %p %v" , len ( mySlice ), cap ( mySlice ), mySlice , mySlice )
执行上面代码后的底层数据结构如下所示:
1
2
len = 5 cap = 5 0xc00008c030 [ 1 2 3 40 5]
len = 3 cap = 4 0xc00008c038 [ 2 3 40]
扩容2:切片扩容后其容量变化
1
2
3
4
5
6
// 创建一个长度和容量都为 5 的切片
mySlice := [] int { 1 , 2 , 3 , 4 , 5 }
fmt . Printf ( "len=%d cap=%d %p %v\n" , len ( mySlice ), cap ( mySlice ), mySlice , mySlice )
// 向切片追加一个新元素,将新元素赋值为 6
mySlice = append ( mySlice , 6 )
fmt . Printf ( "len=%d cap=%d %p %v\n" , len ( mySlice ), cap ( mySlice ), mySlice , mySlice )
执行上面代码后的底层数据结构如下所示:
1
2
len = 5 cap = 5 0xc000018060 [ 1 2 3 4 5]
len = 6 cap = 10 0xc00001c0a0 [ 1 2 3 4 5 6]
注意切片的复用
根据扩容1案例的切片地址可知,mySlice复用slice的底层数据,所以当mySlice追加一个数据的时候,slice原本位置的4变为40,mySlice容量没有变化;
根据扩容2案例可知,当实际需要扩容的时候,会把旧切片数据拷贝到一个新切片中,地址也不一样了;
Go1.18之前的扩容因子与Go1.18之后的有出入,详细的可以去官网去了解;