strings.Builder 比 + 快得多,因其用可增长 []byte 缓冲区避免频繁内存分配与拷贝,而 + 每次拼接都新建字符串;典型场景下性能差5–10倍且GC压力大,且Builder非线程安全。

strings.Builder 为什么比 + 快得多
因为 + 每次拼接都新建字符串,底层触发内存分配和拷贝;而 strings.Builder 内部用可增长的 []byte 缓冲区,只在容量不足时扩容,避免频繁分配。
典型场景:循环中拼接上百次、生成 HTML/JSON 片段、日志组装。这时候 + 的性能可能差 5–10 倍,且 GC 压力明显上升。
注意:strings.Builder 不是线程安全的,不能在 goroutine 间共享使用 —— 这是很多人踩坑的地方。
怎么正确初始化和使用 strings.Builder
别直接声明空结构体,优先用 strings.Builder{} 或带初始容量的 strings.Builder{cap: 256},减少早期扩容次数。
立即学习“go语言免费学习笔记(深入)”;
- 用
builder.WriteString()替代+=,它不复制字符串内容,直接写入缓冲区 - 需要拼接非字符串(如
int、bool)时,先用strconv.Itoa()或fmt.Sprint()转成字符串再写入,不要用fmt.Fprintf(&builder, ...)—— 它会额外包装一层接口,有小开销 - 拼完必须调用
builder.String()获取结果;之后不能再调用WriteString()—— 否则行为未定义(Go 1.22+ 会 panic)
哪些情况不适合用 strings.Builder
拼接项极少(比如就 2–3 个字面量),用 + 更简洁、编译器还能优化;或者拼接逻辑分散在多处、生命周期不清晰,强行用 Builder 反而增加维护成本。
常见误用:
- 在函数里 new 一个
strings.Builder,传指针给多个子函数写入 —— 容易漏掉某处没写、或重复写、或并发写 - 反复创建/重用同一个
Builder却忘了调用builder.Reset(),导致旧内容残留 - 把
builder.Grow(n)当作“保证容量”来用,但没检查是否真够 ——Grow只是提示,不强制,实际仍可能扩容
对比示例:+ vs Builder 实际差异
比如拼接 1000 个 "item":
buf := strings.Builder{}
buf.Grow(4 * 1000) // 预估容量,减少扩容
for i := 0; i < 1000; i++ {
buf.WriteString("item")
}
s := buf.String() // 必须这一步才拿到字符串而 s := "" 循环里 s += "item" 会产生 1000 次堆分配。压测时 Builder 版本分配次数接近 1,+ 版本可能上千次 —— 这直接影响 GC 频率和延迟毛刺。
真正要注意的不是“该不该用”,而是“在哪初始化、谁负责重置、谁负责取结果”。这些边界一旦模糊,性能优势就抵不过逻辑混乱带来的维护成本。











