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
在这个示例中,numbers
和 newNumbers
共享同一个底层数组,内存地址相同,未发生扩容。
容量不足的情况
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
操作可能导致性能问题,应根据实际情况优化代码。