跳到主要内容

Go 切片的扩容机制

append 函数的工作原理

在使用 append 函数向切片添加元素时,Go 会执行一系列操作来确保切片能够容纳新的元素,并保持数据的安全性和性能。

检查容量

首先,append 会检查目标切片的容量是否足以容纳新增的元素。可以使用内置函数 cap 获取切片的容量。

容量足够时

如果容量足够,append 会直接在底层数组的末尾添加新元素,并更新切片的长度。这种情况下,操作是非常高效的,因为不需要分配新的内存或复制数据。

容量不足时

如果容量不足,append 会进行以下操作:

  • 计算新容量:通常情况下,新容量会是旧容量的两倍,但具体增长策略可能会根据切片的长度和类型而有所不同。
  • 分配新数组:分配一个更大的底层数组,以容纳现有元素和新添加的元素。
  • 复制元素:将旧切片的元素复制到新的底层数组中。
  • 添加新元素:在新的底层数组中添加新的元素。
  • 返回新切片append 返回一个指向新底层数组的切片,更新了长度和容量。

返回新切片

需要注意的是,append 返回的是更新后的新切片。即使在容量足够的情况下,返回的也是原切片的引用;但如果发生了扩容,返回的切片将指向新的底层数组。因此,必须使用变量接收 append 的返回值,以确保切片指向正确的底层数组。

切片的扩容机制

切片的容量是其底层数组的容量。以下是切片扩容的具体流程。

底层数组有足够空间

如果切片的底层数组还有足够的空间,Go 会直接在底层数组中添加新元素,并更新切片的长度。这种情况下,底层数组的地址不会改变。

底层数组空间不足

如果底层数组没有足够的空间,Go 会执行以下步骤:

  • 计算新容量:通常为旧容量的两倍,但具体策略可能会根据情况调整。
  • 分配新数组:创建一个容量更大的新数组。
  • 复制数据:将旧切片的数据复制到新数组中。
  • 更新切片引用:切片的指针将指向新的底层数组,长度和容量也会更新。
  • 添加新元素:在新数组中添加新的元素。

切片扩容示例

下面通过代码示例来演示切片的扩容过程。

容量足够的情况

package main

import "fmt"

func main() {
numbers := make([]int, 3, 5)
numbers[0] = 1
numbers[1] = 2
numbers[2] = 3

// 添加两个元素,容量足够,不会触发扩容
newNumbers := append(numbers, 4, 5)

fmt.Printf("numbers=%v, 地址=%p, 长度=%d, 容量=%d\n", numbers, &numbers[0], len(numbers), cap(numbers))
fmt.Printf("newNumbers=%v, 地址=%p, 长度=%d, 容量=%d\n", newNumbers, &newNumbers[0], len(newNumbers), cap(newNumbers))
}

输出:

numbers=[1 2 3], 地址=0xc0000140c0, 长度=3, 容量=5
newNumbers=[1 2 3 4 5], 地址=0xc0000140c0, 长度=5, 容量=5

在这个示例中,numbersnewNumbers 共享同一个底层数组,内存地址相同,未发生扩容。

容量不足的情况

package main

import "fmt"

func main() {
numbers := make([]int, 3, 5)
numbers[0] = 1
numbers[1] = 2
numbers[2] = 3

// 添加三个元素,超出容量,触发扩容
newNumbers := append(numbers, 4, 5, 6)

fmt.Printf("numbers=%v, 地址=%p, 长度=%d, 容量=%d\n", numbers, &numbers[0], len(numbers), cap(numbers))
fmt.Printf("newNumbers=%v, 地址=%p, 长度=%d, 容量=%d\n", newNumbers, &newNumbers[0], len(newNumbers), cap(newNumbers))
}

输出:

numbers=[1 2 3], 地址=0xc0000140c0, 长度=3, 容量=5
newNumbers=[1 2 3 4 5 6], 地址=0xc000018120, 长度=6, 容量=10

由于容量不足,newNumbers 分配了新的底层数组,地址改变,容量扩展为 10。

为什么扩容需要重新申请空间

在 Go 中,切片是对底层数组的引用。当多个切片共享同一个底层数组时,修改其中一个切片的元素会影响到其他切片。如果在容量不足时不分配新的底层数组,直接扩展原数组,会导致多个切片之间的数据混淆,产生不可预期的行为。

通过在扩容时分配新的底层数组,可以保证新切片与原切片的数据隔离,避免数据竞争和内存安全问题。


注意事项

  • 接收 append 返回值:由于 append 可能返回新的切片,一定要使用变量接收返回值。

  • 预分配容量:在需要大量添加元素的情况下,预先分配足够的容量可以减少内存分配次数,提高性能。

  • 切片是引用类型:要注意切片之间共享底层数组的特性,必要时可以使用 copy 函数创建切片的副本。

  • 避免滥用 append:频繁的 append 操作可能导致性能问题,应根据实际情况优化代码。