原文链接: Go 语言切片是如何扩容的?
在 Go 语言中,有一个很常用的数据结构,那就是切片(Slice)。
切片是一个拥有相同类型元素的可变长度的序列,它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
切片是一种引用类型,它有三个属性:指针,长度和容量。

底层源码定义如下:- type slice struct {
- array unsafe.Pointer
- len int
- cap int
- }
复制代码
- 指针: 指向 slice 可以访问到的第一个元素。
- 长度: slice 中元素个数。
- 容量: slice 起始元素到底层数组最后一个元素间的元素个数。
比如使用 make([]byte, 5) 创建一个切片,它看起来是这样的:

声明和初始化
切片的使用还是比较简单的,这里举一个例子,直接看代码吧。- func main() {
- var nums []int // 声明切片
- fmt.Println(len(nums), cap(nums)) // 0 0
- nums = append(nums, 1) // 初始化
- fmt.Println(len(nums), cap(nums)) // 1 1
- nums1 := []int{1,2,3,4} // 声明并初始化
- fmt.Println(len(nums1), cap(nums1)) // 4 4
- nums2 := make([]int,3,5) // 使用make()函数构造切片
- fmt.Println(len(nums2), cap(nums2)) // 3 5
- }
复制代码 扩容时机
当切片的长度超过其容量时,切片会自动扩容。这通常发生在使用 append 函数向切片中添加元素时。
扩容时,Go 运行时会分配一个新的底层数组,并将原始切片中的元素复制到新数组中。然后,原始切片将指向新数组,并更新其长度和容量。
需要注意的是,由于扩容会分配新数组并复制元素,因此可能会影响性能。如果你知道要添加多少元素,可以使用 make 函数预先分配足够大的切片来避免频繁扩容。
接下来看看 append 函数,签名如下:- func Append(slice []int, items ...int) []int
复制代码 append 函数参数长度可变,可以追加多个值,还可以直接追加一个切片。使用起来比较简单,分别看两个例子:
追加多个值:- package main
- import "fmt"
- func main() {
- s := []int{1, 2, 3}
- fmt.Println("初始切片:", s)
- s = append(s, 4, 5, 6)
- fmt.Println("追加多个值后的切片:", s)
- }
复制代码 输出结果为:- 初始切片: [1 2 3]
- 追加多个值后的切片: [1 2 3 4 5 6]
复制代码 再来看一下直接追加一个切片:- package main
- import "fmt"
- func main() {
- s1 := []int{1, 2, 3}
- fmt.Println("初始切片:", s1)
- s2 := []int{4, 5, 6}
- s1 = append(s1, s2...)
- fmt.Println("追加另一个切片后的切片:", s1)
- }
复制代码 输出结果为:- 初始切片: [1 2 3]
- 追加另一个切片后的切片: [1 2 3 4 5 6]
复制代码 再来看一个发生扩容的例子:- package main
- import "fmt"
- func main() {
- s := make([]int, 0, 3) // 创建一个长度为0,容量为3的切片
- fmt.Printf("初始状态: len=%d cap=%d %v\n", len(s), cap(s), s)
- for i := 1; i <= 5; i++ {
- s = append(s, i) // 向切片中添加元素
- fmt.Printf("添加元素%d: len=%d cap=%d %v\n", i, len(s), cap(s), s)
- }
- }
复制代码 运行结果(1.18 版本):- 初始状态: len=0 cap=3 []
- 添加元素1: len=1 cap=3 [1]
- 添加元素2: len=2 cap=3 [1 2]
- 添加元素3: len=3 cap=3 [1 2 3]
- 添加元素4: len=4 cap=6 [1 2 3 4]
- 添加元素5: len=5 cap=6 [1 2 3 4 5]
复制代码 根据上面的结果还是能看到区别的,具体扩容策略下面边看源码边说明。
go1.17
扩容调用的是 growslice 函数,我复制了其中计算新容量部分的代码。
[code]// src/runtime/slice.gofunc growslice(et *_type, old slice, cap int) slice { // ... newcap := old.cap doublecap := newcap + newcap if cap > doublecap { newcap = cap } else { if old.cap < 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 doublecap { newcap = cap } else { const threshold = 256 if old.cap < threshold { newcap = doublecap } else { // Check 0 < newcap to detect overflow // and prevent an infinite loop. for 0 < newcap && newcap < cap { // Transition from growing 2x for small slices // to growing 1.25x for large slices. This formula // gives a smooth-ish transition between the two. newcap += (newcap + 3*threshold) / 4 } // Set newcap to the requested cap when // the newcap calculation overflowed. if newcap |