切片扩容与内存
本教程共 80 篇 · 第 30 篇 · 更新于 2026-07-27 · 约 8 分钟阅读
30. 切片扩容与内存
本节目标:搞懂切片 append 时底层是怎么扩容的,容量增长有什么规律,以及内存共享会踩什么坑。
什么时候会扩容
当 append 往切片加元素时,如果当前 len 没超过 cap,直接加就行,不用扩容。一旦 len 要超过 cap 了,Go 就会分配一个更大的底层数组,把旧数据搬过去。
来看个直观的例子:
package main
import "fmt"
func main() {
s := make([]int, 0, 3)
for i := 1; i <= 5; i++ {
s = append(s, i)
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
}
输出:
len=1 cap=3 [1]
len=2 cap=3 [1 2]
len=3 cap=3 [1 2 3]
len=4 cap=6 [1 2 3 4]
len=5 cap=6 [1 2 3 4 5]
前三次 append 都没超 cap(3),cap 不变。第四次超了,cap 翻倍变成 6。
容量增长规律
切片扩容的策略在 Go 1.18 前后有变化。
Go 1.18 之前的扩容规则比较简单:
- 如果旧容量小于 1024,新容量翻倍
- 如果旧容量大于等于 1024,新容量每次加 1.25 倍
Go 1.18 开始,扩容策略改了。新策略不再简单翻倍,而是根据元素大小和当前容量做更精细的计算。核心思路是:小切片翻倍多一点,大切片增长慢一点,避免浪费太多内存。
大致规律是这样的:
| 旧容量 | 新容量(约) |
|---|---|
| 1 | 2 |
| 2 | 4 |
| 4 | 8 |
| 8 | 16 |
| 16 | 32 |
| … | 翻倍 |
| 256 | 512 |
| 512 | 848 |
| 848 | 1280 |
| 大容量 | 约 1.25 倍 |
Note具体的新容量还和元素类型的大小有关。元素越大,扩容时越保守。这个细节不需要死记,知道「小切片翻倍,大切片放缓」就够了。
扩容后是新的底层数组
扩容时 Go 会分配一个全新的底层数组,把旧数组的数据拷过去。这意味着扩容后的切片和旧切片不再共享内存:
package main
import "fmt"
func main() {
s := make([]int, 3, 3) // len=3, cap=3
copy(s, []int{1, 2, 3})
// s2 和 s 共享底层数组
s2 := s
fmt.Println(s, s2) // [1 2 3] [1 2 3]
// 扩容后 s 指向新数组
s = append(s, 4)
fmt.Println(s, s2) // [1 2 3 4] [1 2 3]
// 改 s 不影响 s2 了
s[0] = 100
fmt.Println(s, s2) // [100 2 3 4] [1 2 3]
}
这个例子值得多看几遍。扩容前 s 和 s2 指向同一个底层数组,改一个另一个也变。append 触发扩容后,s 换了新数组,s2 还指着旧的,两者就分道扬镳了。
内存共享的坑
切片共享底层数组是个好特性,但也容易踩坑。看这个经典案例:
package main
import "fmt"
func main() {
s := []int{1, 2, 3, 4, 5}
// 截取前3个元素
sub := s[:3]
fmt.Println(sub) // [1 2 3]
// 原切片后面的元素其实还在内存里
fmt.Println(s) // [1 2 3 4 5]
// sub 虽然只看到3个,但底层数组有5个
fmt.Println(cap(sub)) // 5
}
sub := s[:3] 只截取了前 3 个元素,但底层数组还是那个 5 元素的数组。只要你不去扩容 sub,它和 s 就共享同一块内存。
更大的坑在这里:如果你有一个很大的切片,只截了一小部分用,但那个大切片一直被引用着,整个大数组都释放不了:
// 一个100万元素的切片
big := make([]int, 1000000)
// 只用前10个
small := big[:10]
// big 不用了,但 small 还在
// 底层的100万元素数组无法被GC回收!
Tip解决办法:用
copy把需要的小部分复制到一个新的独立切片,让大切片可以被回收。
small := make([]int, 10)
copy(small, big[:10])
// 现在 big 可以被回收了
预分配容量避免频繁扩容
如果你知道最终大概要放多少元素,用 make 预分配容量,可以避免多次扩容和拷贝:
// 不好:从空切片开始,多次扩容
var s []int
for i := 0; i < 10000; i++ {
s = append(s, i)
}
// 好:预分配,一次到位
s := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
s = append(s, i)
}
预分配的性能差异在大数据量下很明显。养成这个习惯,代码质量会提升一截。
小结
扩容这块记住三点:
- append 超过 cap 就扩容,扩容后是全新的底层数组
- 小切片翻倍增长,大切片约 1.25 倍(Go 1.18 后更精细)
- 切片共享底层数组,小切片引用大数组可能导致内存泄漏
下一章介绍 Go 1.21 新增的 slices 标准库,排序、查找、克隆一行搞定。