扩容仅在len(s)==cap(s)时触发,核心容量计算为:newcap=oldCap;doublecap=newcap+newcap;若newLen>doublecap则newcap=newLen,否则若oldCap

别直接打开 src/runtime/slice.go 从头啃 growslice —— 你真正该盯住的只有三行容量计算逻辑,其余全是边界防护和内存对齐的“安全带”。
看 growslice 前先确认你遇到的是真扩容
扩容只在 len(s) == cap(s) 时触发。如果 append 后底层数组没换(&s[0] 地址不变),说明根本没走 growslice,此时读源码是白费劲。
- 用
fmt.Printf("%p", &s[0])对比append前后地址,确认是否真的分配了新内存 - 小 slice(
cap )翻倍增长:newcap = oldcap * 2 - 大 slice(
cap >= 256)按1.25倍增长,再向上 8 字节对齐:比如cap=1024 → newcap=1280,不是2048 - 空 slice(
cap == 0)首次append直接设newcap = 1,不翻倍
growslice 里真正决定容量的那几行在哪
在 src/runtime/slice.go 的 growslice 函数中,核心就这几行(Go 1.21+):
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
newLen
- 别被
for循环吓到——它只是反复加1/4直到够用,等效于 “向上取整到 1.25 倍” -
newcap算出来后,runtime 还会做 8 字节对齐(roundupsize),所以cap值往往不是整数倍 - 如果元素含指针(如
[]*int),扩容路径还会插入写屏障逻辑,影响 GC 停顿,但不影响容量计算本身
为什么 make([]T, 0, N) 预分配有时也不保险
预分配能避开大部分扩容,但 runtime 仍可能“多分一点”——特别是你一次性 append 多个元素时,growslice 会按 newLen 而非你预估的 N 计算新容量。
- 例如:
s := make([]int, 0, 10),然后s = append(s, []int{1,2,3,4,5,6,7,8,9,10,11}...)(共 11 个),newLen = 11,oldCap = 10,触发扩容,最终cap可能变成16或20,取决于起始cap是否 - 想完全锁死容量?用三参数切片表达式:
s := make([]int, 0, 10); s = s[:0:10],这样后续append超过 10 就必然分配新底层数组,不会复用剩余空间 - 调试时别依赖
cap精确等于预设值,用cap(s) >= N判断更稳妥
真正容易被忽略的点:扩容策略只管“够不够放”,不管“放得有多松”。哪怕你只差 1 个元素,runtime 也可能给你翻倍或加 25%,这不是 bug,是为后续追加预留 buffer。所以观察行为,永远以 &s[0] 地址变化 + cap() 实际返回值为准,别靠脑补倍数。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










