切片看起来像是一个动态的数组,并没有什么好讲的。但在实际工作中,确实会遇到一些实际问题,因此有必要对切片和数组的实现做一些简单了解。
数组
数组是一个固定长度的数据类型,相较于切片,数组无法通过 append 方法来动态增加元素。
1
2
|
// 声明一个长度为 5 的数组
var newArray [5]int
|
上述代码声明了一个长度为 5 的整型数组,并且数组的元素都被初始化为 0。如果需要添加元素,只能重新声明一个更大的数组。
另外需要注意的是,上述数组的类型为 [5]int。对于 Go 来说,存储元素类型相同,但是大小不同的数组也是不同的类型,只有两个条件都相同才是同一类型。
切片
1
2
3
4
5
6
7
8
|
// 声明一个切片
var newSlice []int
// 使用 make 函数创建一个空切片
newSlice := make([]int, 0)
// 使用 make 函数创建一个长度为 5 的切片
newSlice := make([]int, 5)
// 使用 make 函数创建一个长度为 5,容量为 10 的切片
newSlice := make([]int, 5, 10)
|
以上代码分别声明了一个空切片,一个长度为 5 的切片,一个长度为 5,容量为 10 的切片。如果还没了解过切片的话,可能会对长度和容量感到困惑,这需要对切片的内部结构有一定了解。
切片的结构
切片是一个可变长度的数组,它的内部结构如下:
1
2
3
4
5
|
type slice struct {
array unsafe.Pointer
len int
cap int
}
|
如上,切片包含了一个指向数组的指针,切片的长度,和底层数组的容量。
- 切片的长度是指切片中元素的个数
- 切片的容量是指切片开始位置到底层数组的最后位置的长度
我们通过一些图示来进行说明,我们先创建如下切片:
1
2
3
4
|
newArray := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
newSlice := newArray[2:5]
fmt.Println(newSlice, len(newSlice), cap(newSlice))
// 输出:[2 3 4] 3 6
|
我们创建了一个长度为 8 的数组,然后创建了一个切片,切片的起始位置是数组的第 2 个元素,结束位置是数组的第 5 个元素。切片的长度是 3,容量是 6。
下图展示了切片的结构:

需要注意,此时切片和数组共享底层数组,因此修改切片的元素会影响到数组的元素。
1
2
3
|
newSlice[0] = 100
fmt.Println(newArray)
// 输出:[0 1 100 3 4 5 6 7]
|
如果需要避免这种情况,可以使用 copy 函数来创建一个新的切片。
1
2
3
4
5
6
7
8
|
// 创建一个新的切片,长度为 3。
// 在使用 copy 时,目标切片需要分配足够的空间来容纳被复制的元素
newSlice := make([]int, 3)
copy(newSlice, newArray[2:5])
newSlice[0] = 100
fmt.Println(newSlice)
fmt.Println(newArray)
// 输出:[0 1 2 3 4 5 6 7]
|
切片的扩容
切片相较于数组的优势在于可以动态增加元素。我们已经知道切片的底层是一个数组,那么当底层数组的容量不足以容纳新的元素时,切片就会发生扩容。
切片的扩容是一个比较耗时的操作,因为它需要重新分配内存,并且将原来的元素复制到新的内存中。
切片的扩容策略是:
- 如果期望的容量大于当前容量的两倍,那么新的容量就是期望的容量
- 如果当前切片的长度小于 1024,那么新的容量就是当前容量的两倍
- 如果当前切片的长度大于等于 1024,那么会每次增加 25% 的容量,直到新的容量大于等于期望的容量
以上策略会确定切片大致容量,实际执行中还会根据内存对齐等因素进行调整。
可以根据以下代码来测试切片的扩容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
func main() {
// 初始化一个长度为 0 的切片
s := make([]int, 0)
// 打印切片的长度和容量
for range 100 {
fmt.Println(len(s), cap(s))
// 向切片中添加一个元素
s = append(s, 1)
}
}
// 输出
// 0 0
// 1 1
// 2 2
// 3 4
// 4 4
// 5 8
// 6 8
// 7 8
// 8 8
// 9 16
// 10 16
// ...
|
切片的频繁扩容会影响程序的性能,因此在实际开发中,应该尽量避免频繁扩容。可以通过预先分配足够的容量来避免切片的频繁扩容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
func main() {
// 初始化一个长度为 0,容量为 100 的切片
s := make([]int, 0, 100)
// 打印切片的长度和容量
for range 100 {
fmt.Println(len(s), cap(s))
// 向切片中添加一个元素
s = append(s, 1)
}
}
// 输出
// 0 100
// 1 100
// 2 100
// 3 100
// 4 100
// 5 100
// 6 100
// 7 100
// 8 100
// 9 100
// 10 100
// ...
|
参考