cap参数只控制底层数组容量,不改变len;append在len≤操作≤cap时不扩容、不复制。常见误以为make([]int,0,10)有10个元素,实际len=0、cap=10,可append10次零开销。

make 切片时 cap 参数到底控制什么
它只控制底层数组的容量,不改变 len;后续 append 超过 len 但没超过 cap 时,不会分配新内存,也不会复制原数据。
常见错误是以为 make([]int, 0, 10) 创建了一个“有10个元素的空切片”,其实它的 len 是 0,cap 是 10 —— 你可以连续 append 10 次都不触发扩容。
-
make([]T, len):等价于make([]T, len, len),cap = len -
make([]T, 0, n):最常用预分配写法,len=0,cap=n,适合后续循环 append -
make([]T, n, m)(m > n):创建带初始元素的切片,但 cap 更大,比如预留空间给后续拼接
哪些场景必须预分配 cap
高频 append 且长度可预估时,不预分配会导致多次底层数组拷贝,性能断崖式下跌。典型如读文件行、解析 JSON 数组、批量构建查询参数。
一个真实例子:append 10 万次 int,未预分配平均耗时 ~12ms,make([]int, 0, 100000) 后再 append,耗时稳定在 ~3ms —— 差 4 倍不是玄学,是 memcpy 次数从约 17 次降到 0 次。
- 已知最终长度(如数据库查出 5000 行)→ 直接
make([]X, 0, 5000) - 长度有上限但不确定(如 HTTP header 最多 100 个)→ 按上限预分配,避免最坏扩容
- 拼接多个小切片(
dst = append(dst, src...))→ 确保dst的 cap 足够,否则每次 append... 都可能扩容
cap 预估不准的后果和补救
预分配过大浪费内存,过小仍会扩容——但后者更危险:一旦扩容发生,旧底层数组可能被 GC 滞留(尤其存了大量指针或大结构体时),造成意外内存占用。
Go 1.22+ 对小切片扩容策略做了优化,但对 >2KB 的切片,扩容仍是 2 倍增长,很容易跳到 4KB → 8KB → 16KB…
- 保守做法:用
max(expected_len, 16)作为 cap 下限,避开小尺寸抖动 - 动态调整:如果发现某次 append 后
cap远大于len(比如 cap/len > 4),可以显式截断:s = s[:len(s):len(s)] - 注意
append返回新切片,原变量不更新:错误写法append(s, x); fmt.Println(len(s))—— 此时s没变,要赋值s = append(s, x)
和 slice 字面量、copy 的配合技巧
预分配 + copy 比反复 append 更快,尤其当源数据已存在(如从 buffer 读取固定长度二进制块)。
例如解析网络包:先 make([]byte, 0, packetLen),再 copy(buf, packetData),比循环 append(buf, b) 快 3–5 倍,且无中间分配。
- 字面量
[]int{1,2,3}的 cap = len = 3,无法扩展,别误当可增长容器用 -
copy(dst, src)要求dst容量足够,否则只复制 min(len(dst), len(src)) 个元素,不报错也不提示 - 用
s[:0]清空切片(保留底层数组)比重新make更省,适合复用场景(如连接池中的缓冲区)
make 多写两个参数,但实际影响的是内存布局连续性、GC 压力、甚至并发安全(扩容时若其他 goroutine 正在读原底层数组,可能看到部分更新)。真正难的不是写对语法,而是判断“这里到底需不需要预分配”以及“该预多少”。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











