Go 语言切片是如何扩容的?

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

Go 语言切片是如何扩容的?

文章插图
底层源码定义如下:
type slice struct {array unsafe.Pointerlenintcapint}
  1. 指针: 指向 slice 可以访问到的第一个元素 。
  2. 长度: slice 中元素个数 。
  3. 容量: slice 起始元素到底层数组最后一个元素间的元素个数 。
比如使用 make([]byte, 5) 创建一个切片 , 它看起来是这样的:
Go 语言切片是如何扩容的?

文章插图
声明和初始化切片的使用还是比较简单的 , 这里举一个例子 , 直接看代码吧 。
func main() {var nums []int// 声明切片fmt.Println(len(nums), cap(nums)) // 0 0nums = Append(nums, 1)// 初始化fmt.Println(len(nums), cap(nums)) // 1 1nums1 := []int{1,2,3,4}// 声明并初始化fmt.Println(len(nums1), cap(nums1))// 4 4nums2 := make([]int,3,5)// 使用make()函数构造切片fmt.Println(len(nums2), cap(nums2))// 3 5} 扩容时机当切片的长度超过其容量时 , 切片会自动扩容 。这通常发生在使用 append 函数向切片中添加元素时 。
扩容时 , Go 运行时会分配一个新的底层数组 , 并将原始切片中的元素复制到新数组中 。然后 , 原始切片将指向新数组 , 并更新其长度和容量 。
需要注意的是 , 由于扩容会分配新数组并复制元素 , 因此可能会影响性能 。如果你知道要添加多少元素 , 可以使用 make 函数预先分配足够大的切片来避免频繁扩容 。
接下来看看 append 函数 , 签名如下:
func Append(slice []int, items ...int) []intappend 函数参数长度可变 , 可以追加多个值 , 还可以直接追加一个切片 。使用起来比较简单 , 分别看两个例子:
追加多个值:
 
package mainimport "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 mainimport "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 mainimport "fmt"func main() {s := make([]int, 0, 3) // 创建一个长度为0 , 容量为3的切片fmt.Printf("初始状态: len=%d cap=%d %vn", len(s), cap(s), s)for i := 1; i <= 5; i++ {s = append(s, i) // 向切片中添加元素fmt.Printf("添加元素%d: len=%d cap=%d %vn", i, len(s), cap(s), s)}} 
输出结果为:
 
初始状态: 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] 
在这个例子中 , 我们创建了一个长度为 0? , 容量为 3? 的切片 。然后 , 我们使用 append? 函数向切片中添加 5 个元素 。
当我们添加第 4? 个元素时 , 切片的长度超过了其容量 。此时 , 切片会自动扩容 。新的容量是原始容量的两倍 , 即 6 。
表面现象已经看到了 , 接下来 , 我们就深入到源码层面 , 看看切片的扩容机制到底是什么样的 。
源码分析在 Go 语言的源码中 , 切片扩容通常是在进行切片的 append? 操作时触发的 。在进行 append? 操作时 , 如果切片容量不足以容纳新的元素 , 就需要对切片进行扩容 , 此时就会调用 growslice 函数进行扩容 。


推荐阅读