strings.Builder扩容策略为cap×2+needed,非简单翻倍;Grow(n)仅预留空间,实际可用空间为Cap()-Len();Reset()不清空底层数组,易致内存驻留。

strings.Builder 的扩容不是简单翻倍
很多人看到 strings.Builder 底层用 []byte 就默认它和普通 slice 一样走「cap × 2」策略,其实不对。它的扩容公式是 cap × 2 + needed,其中 needed 是本次写入的字节数,不是固定值。
这意味着:
- 初始
cap为 0,第一次WriteString("a")后,cap变成 8(硬编码在src/strings/builder.go) - 接着写入 100 字节,当前
cap == 8,不够,就按8×2+100 = 116分配新底层数组,不是 16 - 如果提前
Grow(1024),后续写入 ≤1024−b.Len()字节就不会再扩容
Grow(n) 不分配 n 字节,只确保剩余空间 ≥ n
Grow(n) 的作用是「预留」,不是「分配」。它检查的是 b.Cap() - b.Len() >= n,不满足才触发扩容;满足就什么也不做。
常见误用场景:
立即学习“go语言免费学习笔记(深入)”;
-
b.Grow(1024)后只写入 1 字节,b.Cap()仍是 1024 —— 这没问题,但别以为还能再安全写 1024 字节 - 实际可用空间是
b.Cap() - b.Len(),不是n - 在循环里反复调用
b.Grow(1024)没意义:容量不会重置,也不会叠加
Cap() 和 Len() 的差值才是关键判断依据
b.Len() 是已写入字节数(即最终字符串长度),b.Cap() 是底层数组总容量。两者之差才是当前剩余空间。
容易踩的坑:
- 用
b.Cap() == 0判断 builder 是否为空 → 错,应该用b.Len() == 0 - 拼接前没估算总长,只靠默认行为 → 首次写入小字符串后,紧接着大块内容会触发一次非必要扩容
- 复用 long-lived
Builder实例时,Reset()只清len,不释放底层数组 → 内存驻留,GC 压力隐性上升
为什么比 bytes.Buffer 更紧凑,但也更难预估
strings.Builder 和 bytes.Buffer 虽然都用 []byte,但语义和扩容逻辑不同:
-
bytes.Buffer默认cap == 64,首次扩容直接翻倍到 128;strings.Builder初始cap == 0,更激进地节省初始内存 - 拼接总长可控(如生成固定结构 JSON)时,
Builder内存占用更低 - 但拼接长度波动极大(1B 到 1MB 交替)时,它的动态策略可能比预设大 cap 的
bytes.Buffer多触发几次扩容 - 二者都不支持缩容,
Reset()后底层数组仍保留在堆上
实测时别只信文档,直接打印 b.Cap() 看变化最可靠——尤其在拼接模式不规则的场景下,一次扩容的代价可能远超多调几次 Grow()。



















