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

打印 上一主题 下一主题

主题 1789|帖子 1789|积分 5367

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

底层源码定义如下:
  1. type slice struct {
  2.     array unsafe.Pointer
  3.     len   int
  4.     cap   int
  5. }
复制代码

  • 指针: 指向 slice 可以访问到的第一个元素。
  • 长度: slice 中元素个数。
  • 容量: slice 起始元素到底层数组最后一个元素间的元素个数。
比如使用 make([]byte, 5) 创建一个切片,它看起来是这样的:

声明和初始化

切片的使用还是比较简单的,这里举一个例子,直接看代码吧。
  1. func main() {
  2.     var nums []int  // 声明切片
  3.     fmt.Println(len(nums), cap(nums)) // 0 0
  4.     nums = append(nums, 1)   // 初始化
  5.     fmt.Println(len(nums), cap(nums)) // 1 1
  6.     nums1 := []int{1,2,3,4}    // 声明并初始化
  7.     fmt.Println(len(nums1), cap(nums1))    // 4 4
  8.     nums2 := make([]int,3,5)   // 使用make()函数构造切片
  9.     fmt.Println(len(nums2), cap(nums2))    // 3 5
  10. }
复制代码
扩容时机

当切片的长度超过其容量时,切片会自动扩容。这通常发生在使用 append 函数向切片中添加元素时。
扩容时,Go 运行时会分配一个新的底层数组,并将原始切片中的元素复制到新数组中。然后,原始切片将指向新数组,并更新其长度和容量。
需要注意的是,由于扩容会分配新数组并复制元素,因此可能会影响性能。如果你知道要添加多少元素,可以使用 make 函数预先分配足够大的切片来避免频繁扩容。
接下来看看 append 函数,签名如下:
  1. func Append(slice []int, items ...int) []int
复制代码
append 函数参数长度可变,可以追加多个值,还可以直接追加一个切片。使用起来比较简单,分别看两个例子:
追加多个值:
  1. package main
  2. import "fmt"
  3. func main() {
  4.     s := []int{1, 2, 3}
  5.     fmt.Println("初始切片:", s)
  6.     s = append(s, 4, 5, 6)
  7.     fmt.Println("追加多个值后的切片:", s)
  8. }
复制代码
输出结果为:
  1. 初始切片: [1 2 3]
  2. 追加多个值后的切片: [1 2 3 4 5 6]
复制代码
再来看一下直接追加一个切片:
  1. package main
  2. import "fmt"
  3. func main() {
  4.     s1 := []int{1, 2, 3}
  5.     fmt.Println("初始切片:", s1)
  6.     s2 := []int{4, 5, 6}
  7.     s1 = append(s1, s2...)
  8.     fmt.Println("追加另一个切片后的切片:", s1)
  9. }
复制代码
输出结果为:
  1. 初始切片: [1 2 3]
  2. 追加另一个切片后的切片: [1 2 3 4 5 6]
复制代码
再来看一个发生扩容的例子:
  1. package main
  2. import "fmt"
  3. func main() {
  4.     s := make([]int, 0, 3) // 创建一个长度为0,容量为3的切片
  5.     fmt.Printf("初始状态: len=%d cap=%d %v\n", len(s), cap(s), s)
  6.     for i := 1; i <= 5; i++ {
  7.         s = append(s, i) // 向切片中添加元素
  8.         fmt.Printf("添加元素%d: len=%d cap=%d %v\n", i, len(s), cap(s), s)
  9.     }
  10. }
复制代码
运行结果(1.18 版本):
  1. 初始状态: len=0 cap=3 []
  2. 添加元素1: len=1 cap=3 [1]
  3. 添加元素2: len=2 cap=3 [1 2]
  4. 添加元素3: len=3 cap=3 [1 2 3]
  5. 添加元素4: len=4 cap=6 [1 2 3 4]
  6. 添加元素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

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

南飓风

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表