strings.Builder.Grow(n)仅确保剩余空间≥n,不改变长度或内容;扩容公式为cap×2+needed;Reset()不释放底层数组,cap不变;真实剩余空间为Cap()-Len()。

strings.Builder.Grow() 不是分配 n 字节,而是预留剩余空间
Grow(n) 的作用常被误解为“让容量变成当前 cap + n”,实际它只确保 b.Cap() - b.Len() >= n。不满足才触发扩容,满足就什么也不做。比如 b.Len() 是 50、b.Cap() 是 128,调用 Grow(64) 不会扩容(128 − 50 = 78 ≥ 64),但 Grow(80) 就会触发。
关键点在于:Grow() 不改变已写入内容,也不重置长度,它只干预底层数组是否需要重新分配。多次调用相同参数的 Grow()(如循环里反复 b.Grow(1024))毫无意义——容量不会叠加,也不会清零。
扩容公式不是 cap × 2,而是 cap × 2 + needed
很多人以为 strings.Builder 和普通 slice 一样走翻倍策略,其实它的扩容逻辑硬编码在 src/strings/builder.go 中:新 cap = 当前 cap × 2 + 本次写入所需字节数(needed)。这意味着:
- 初始空 builder 写入 1 字节后,cap 直接变成
8(固定起点) - 当前 cap = 8,再写入 100 字节 → 新 cap = 8 × 2 + 100 =
116,不是16 - 如果提前
Grow(1024),后续所有写入只要总长 ≤ 1024 就不会触发扩容,但实际可用空间仍是b.Cap() - b.Len(),不是1024
Reset() 不清空底层数组,可能导致内存驻留
Reset() 只把 b.Len() 设为 0,底层数组([]byte)完全不动,cap 保持不变。这带来两个实际影响:
立即学习“go语言免费学习笔记(深入)”;
- 复用 builder 时,如果之前膨胀到 10MB,
Reset()后 cap 还是 10MB,下次写入哪怕只有几字节,也继续用这 10MB 底层空间 - 在长期运行的服务中,若 builder 被高频复用且曾写入大字符串,容易造成内存无法释放,表现为 RSS 持续上涨
- 想真正释放内存,只能新建 builder 或显式置空引用(如
var b strings.Builder重新声明)
Cap() 和 Len() 的差值才是真实剩余空间
判断还能写多少字节,唯一可靠指标是 b.Cap() - b.Len()。别依赖 b.Cap() 单独看,也别误以为 Grow(n) 后就能安全写 n 字节——它只保证“至少能写 n 字节”,但写完后 b.Len() 增加,剩余空间立刻减少。
典型误用:
- 调
b.Grow(1024)后直接循环b.WriteString("a")1024 次 → 第 1024 次可能触发扩容(因为每次写都增加b.Len(),剩余空间持续收窄) - 用
b.Len()当作已分配内存估算 → 错,b.Len()是内容长度,b.Cap()才是已分配总空间 - 以为
b.Reset()后b.Cap()会变小 → 不会,cap 不变,数组没丢
真正要控内存上限,得自己封装 LimitedBuilder,在 WriteString() 前校验 used + len(s) ,而不是指望扩容机制兜底。


















