append 在 len(s) == cap(s) 时真正扩容,此时 runtime 按分段策略(cap<1024则翻倍,≥1024则增1/4并8字节对齐)分配新底层数组并拷贝数据。
append 什么时候真正扩容?
只在
的瞬间触发扩容,不是每次调用都分配新内存。很多人误以为“一 append 就扩容”,结果在循环里写
却没预估容量,导致底层数组反复拷贝。
空 slice(
)首次
会直接分配 1 个元素空间,不是 0
时,无论
是 5 还是 5000,都不会扩容,直接写入原底层数组末尾
扩容临界点是
,这不是错误状态,而是 runtime 判断“该换地方了”的唯一信号
如果一次性
,runtime 会先算出“需要至少容纳 len+3 的容量”,再按规则扩到 ≥ 该值的最小合法值
扩容倍数到底是多少?别硬套“翻倍”
Go 的扩容策略藏在
里,实际分段处理,并做 8 字节对齐:
:新
(例如 512 → 1024)
:新
,再向上取整到 8 的倍数(例如 2048 → 2560,3000 → 3760)
如果期望容量远超当前值(比如
但要追加 1000 个元素),runtime 会跳过倍增逻辑,直接按需分配 ≥ 期望值的最小合法容量
所有新容量都会向上对齐到 8 字节边界,这是为了内存分配器效率,不是随意设计
make([]T, 0, n) 是最便宜的优化
靠 runtime 自动扩容永远有代价:分配、拷贝、GC 压力。而
预留空间,后续 256 次
全部免扩容,几乎零成本。
已知最终长度约 200?直接
从已有数组切片(如
)再追加?注意它的
可能只有 100,不够就会脱离原数组,失去共享优势
错误示范:
—— 至少触发 10 次扩容,底层数组拷贝累计超 100KB
调试时打印
,这才是真实余量;只看
容易误判
并发写 slice 必须自己加锁
多个 goroutine 共享一个 slice 并调用
,即使
足够,也可能因竞态导致数据覆盖或 panic——因为
返回新结构体,赋值操作非原子,且底层写入位置可能重叠。
立即学习
“
go语言免费学习笔记(深入)
”;
永远不就地修改原 slice,它返回新结构体,必须显式赋值才能生效
函数内要追加,得让函数返回新 slice:
复用缓冲区(如网络包解析)时,反复用
,前一次残留数据可能被后续读到;安全分离方式是
或
真正容易被忽略的是:扩容不是性能瓶颈本身,而是它暴露了容量预估缺失 + 共享误用 + 并发裸奔这三个深层问题。盯着
和
差值看,比猜更可靠。
appendlen(s) == cap(s)s = append(s, x)cap == 0appendlen(s) < cap(s)caplen == capappend(s, a, b, c)src/runtime/slice.gocap < 1024cap = old cap * 2cap >= 1024cap = old cap + old cap / 4cap=100make([]int, 0, 256)appendmake([]int, 0, 256)data[100:200]caps := make([]int, 0); for i := 0; i < 1000; i++ { s = append(s, i) }cap - lenlenappendcapappendappendfunc add(s []int, x int) []int { return append(s, x) }buf[off:off+n]newS := append([]byte(nil), oldS)copylencap