预分配切片是高频 append 场景下的必做动作;需区分 make([]t, 0, n)(仅分配底层数组,len=0,cap=n,适合流式构建)与 make([]t, n)(初始化 n 个零值,len=cap=n,适合直接索引赋值)。

预分配切片不是“可选优化”,而是高频 append 场景下的必做动作;不预分配,runtime.growslice 就会反复触发内存分配和拷贝,性能直接掉档。
make([]T, 0, n) 和 make([]T, n) 的区别必须分清
前者只分配底层数组、len=0、cap=n,后续 append 直接复用;后者分配数组并写入 n 个零值、len=cap=n,第一次 append 就得扩容——这和你要的效果完全相反。
-
make([]int, 0, 1000):适合流式构建,比如读文件行、解析 JSON 数组,append1000 次都零扩容 -
make([]int, 1000):适合已知固定长度且需直接索引赋值的场景,如生成 1000 个随机数:for i := range s { s[i] = rand.Int() } - 对指针切片如
[]*User,make([]*User, n)会初始化n个nil,再append是往末尾加新元素,不是填空——前n个仍是nil
预分配后怎么填数据:索引赋值 vs append 要看类型和意图
填法错,预分配就白做了。核心判断依据是:你是否需要“覆盖已有位置”还是“追加到末尾”。
- 值类型 + 固定长度 → 用
make([]T, n)+ 索引循环赋值(快、无额外分配) - 指针/结构体切片 + 需避免
nil占位 → 用make([]*T, 0, n)+append(安全、语义清晰) - 混合场景(如先读 header 再拼 body)→ 用
make([]byte, 0, totalLen),然后copyheader,再appendbody 片段 - 错误示范:
s := make([]*T, 5); for _ = range s { s = append(s, &T{}) }→ 结果是 5 个nil+ 5 个新对象,len=10
容量估不准时的容错策略
宁可略大,别吝啬那点内存;但也不能无脑堆大,否则 GC 压力反升。
- 有明确上限(如协议字段 ≤16)→ 直接
cap = 16 - 常见规模在 10–200 之间 → 用
cap = 256,覆盖绝大多数情况,成本可控 - 完全未知但高频调用(如日志 buffer)→ 放进
sync.Pool,按 size 分池,Put前确保Reset - 发现
cap / len > 4且长期如此 → 显式截断:s = s[:len(s):len(s)],回收多余容量
容易被忽略的共享与复用陷阱
预分配的底层数组一旦被其他变量引用,append 就会强制新建——这不是 bug,是 Go 的设计逻辑。
- 不要在循环里把子切片传给 goroutine 后还继续往原切片
append,如:sub := data[i:i+10]; go f(sub)→data底层数组被持有,下次append必然 malloc 新数组 - 纯值类型切片(
[]int,[]byte)复用可直接s = s[:0] - 含指针或嵌套 slice/map 的结构体切片 → 必须遍历置零:
for i := range s { s[i] = MyStruct{} },否则旧指针残留,GC 不敢回收 -
append返回新切片,原变量不会自动更新:append(s, x)不改变s,必须写成s = append(s, x)
最常踩的坑不在语法,而在对 len/cap 分离的理解偏差,以及对“底层数组是否被外部持有”的盲区——这两点不厘清,预分配就只是自我安慰。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











