append仅在len(s)==cap(s)时触发扩容,此时按cap

扩容只在 len(s) == cap(s) 时发生,不是每次 append 都重分配;真正影响性能的不是“扩不扩容”,而是你没盯住 cap - len 这个差值。
为什么 append 有时快有时慢?
因为 append 本身不分配内存——它只在当前长度撑满容量(len(s) == cap(s))的瞬间才调用 growslice。此前所有追加都只是写入底层数组末尾,零分配、零拷贝。
- 常见错误现象:循环里写
s = append(s, x)却没预估容量,结果从cap=1开始,触发1→2→4→8→16…多次重分配和 memmove - 空切片
s := []int{}首次append直接分配 1 个元素空间,不是 0 - 用
make([]int, 0, 1024)初始化后,前 1024 次append全部免扩容,结构体复制 + 写入即完成
growslice 怎么算新容量?
规则藏在 src/runtime/slice.go,不是简单翻倍,而是分段 + 对齐:
cap :新 <code>cap = old cap * 2(如 512 → 1024)-
cap >= 1024:新cap = old cap + old cap / 4,再向上对齐到 8 的倍数(如 2048 → 2560,3000 → 3760) - 一次性追加多个元素(
append(s, a, b, c)):先算出“至少需要len+3容量”,再按上述规则扩到 ≥ 该值的最小合法值 - 所有新容量最终都会被向上对齐到 8 字节边界,这是为内存分配器效率服务,不是随意设计
为什么切片共享底层数组会出问题?
因为 append 返回的是新结构体,但它的 array 字段可能指向原数组,也可能指向新分配的数组——一旦扩容,就脱离原数组,其他仍指向旧地址的切片就变成“悬空视图”。
- 典型场景:从大缓冲区切出多个子切片用于网络包解析,其中一个子切片
append触发扩容,其余子切片仍读旧内存,可能拿到脏数据或 panic - 安全复用方式:用
newS := append([]byte(nil), oldS...)强制深拷贝,或显式copy到新底层数组 - 完整切片表达式
arr[low:high:max]中的max是原数组索引,不是容量数字;它能限制后续append的上限,避免意外覆盖
并发写切片为什么必须加锁?
因为 append 不是原子操作:它先判断是否扩容、再 malloc/memmove、最后构造并返回新结构体。多个 goroutine 同时调用,即使 cap 足够,也可能因竞态导致:
- 两个 goroutine 同时发现
len == cap,都去分配新数组,其中一份被丢弃 - 一个 goroutine 写入底层数组位置 A,另一个写入位置 B,但 B 覆盖了 A 的后续字节(尤其在写
[]byte时) - 赋值
s = append(s, x)本身非原子:读旧结构体 → 计算 → 写新结构体,中间可能被抢占
真正难的不是记住公式,而是意识到:每次 append 都可能是 malloc + memmove,而你在 pprof 里看到时,已经晚了。盯紧 cap - len,比猜“差不多该满了”可靠得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











