预分配容量可避免 append 频繁扩容导致的性能下降;Go slice 底层为数组,容量不足时 growslice 会分配更大数组并拷贝数据,引发 O(n) 均摊开销与内存抖动。
预分配容量是提升 append 性能最直接有效的方式,否则频繁扩容会触发多次底层数组复制。
append 会变慢?Go 的 slice 底层是数组,当容量不足时,append 会调用 growslice 分配新数组(通常是旧容量的 1.25–2 倍),再把旧数据拷贝过去。这个过程在循环中反复发生,时间复杂度从 O(1) 退化为均摊 O(n),且伴随内存抖动。
pprof 显示大量 runtime.makeslice 或 runtime.growslice 调用append 本身慢,而是未预估长度导致反复 reallocmake 的三个参数make([]T, len, cap) 中 cap 决定初始容量;只要最终长度 ≤ cap,整个追加过程零扩容。
items := make([]int, 0, 1000)
for i := 0; i < 1000; i++ {
items = append(items, i)
}logs := make([]string, 0, 100000)
scanner := bufio.NewScanner(file)
for scanner.Scan() {
logs = append(logs, scanner.Text())
}len 当 cap 初始化写成 make([]T, 0, len(src)) 是常见误操作——它只保证容量够存当前数据,但若后续还要追加,依然会扩容。
src := []int{1,2,3}
dst := make([]int, 0, len(src)) // cap=3
dst = append(dst, src...) // ok,len=3, cap=3
dst = append(dst, 99) // 触发扩容!因为 cap 已满cap,或用 copy + append 组合:dst := make([]int, len(src)+1) copy(dst, src) dst[len(src)] = 99
B 数据,每次复制就是 10MB 内存拷贝高频短生命周期切片(如网络包解析)适合复用底层数组,减少 GC 压力。
[:0] 清空:func parsePacket(buf []byte, out *[]string) {
*out = (*out)[:0] // 复用底层数组,不释放内存
// ... 解析逻辑,往 *out 追加
}
var cache []string
parsePacket(packet, &cache)cache 不逃逸到长期作用域,否则阻碍 GC真正影响性能的从来不是 append 函数调用本身,而是你有没有在分配时就告诉 Go “我大概要装多少东西”。容量预估偏差 20% 通常比每次都从 0 开始强得多。