Golangbytes.Buffer与strings.Builder如何选择

strings.Builder 更适合纯字符串拼接,轻量高效、零值安全、避免内存逃逸;bytes.Buffer 功能更全,支持I/O接口、中间读取和多种写入方式,但有额外开销。

strings.Builder 更适合纯字符串拼接

当目标是构建一个最终为 string 的结果,且过程中不涉及二进制数据、不需读取中间状态、也不需要像 io.Reader 那样流转时,strings.Builder 是更轻量、更高效的选择。它底层复用 []byte,但禁止直接访问底层数组,避免了意外修改和内存逃逸。

  • 构造开销小:strings.Builder{} 是零值安全的,无需初始化容量(当然预估长度并调用 Grow() 能进一步减少扩容)
  • 写入方法仅支持 string[]byte(后者会转成字符串),不支持单个 byterune —— 如果你需要频繁写入字节或 rune,得自己转换
  • 没有 Len()Bytes() 方法,只有 String();一旦调用 String(),内部底层数组可能被共享(Go 1.18+ 已优化为只读拷贝,但仍建议避免在 String() 后继续写入)
var b strings.Builder
b.Grow(128)
b.WriteString("hello")
b.WriteString(" ")
b.WriteString("world")
result := b.String() // 此后不应再对 b 调用 Write* 方法

bytes.Buffer 适用场景更广,但有额外成本

bytes.Buffer 实现了 io.Readerio.Writerio.ByteReaderio.RuneScanner 等多个接口,能无缝接入标准库 I/O 流程。如果你需要把拼接过程当作流处理(比如传给 json.Encodertemplate.Execute、或做部分读取),它几乎是唯一选择。

  • 支持所有基础写入:单个 byteWriteByte())、runeWriteRune())、string[]byte,也支持 fmt.Fprintf(b, "...", x)
  • 可随时读取当前内容:b.Bytes() 返回可变切片,b.String() 返回只读副本;注意 Bytes() 返回的切片会随后续写入失效(底层数组可能被扩容复制)
  • Reset()Truncate()Read() 等操作,灵活性高,但也带来额外字段和方法调用开销
var b bytes.Buffer
b.WriteString("value: ")
fmt.Fprint(&b, 42)
b.WriteByte('\n')
// 可以接着传给其他函数:
json.NewEncoder(&b).Encode(map[string]int{"x": 1}) // ❌ 错误:Encoder 需要 io.Writer,但这里 b 已含前置内容
// 正确做法是另起一个 Buffer,或用 Reset()

别在 strings.Builder 上调用 Bytes() 或试图取地址

strings.Builder 没有公开的 Bytes() 方法,强行通过反射或 unsafe 获取底层数组属于未定义行为,且 Go 1.20+ 对其内部结构做了调整,极易出错。如果业务逻辑中突然需要「在拼接中途读取原始字节」或「把 Builder 当作 buffer 复用」,说明设计上已偏离它的定位 —— 这时应直接换用 bytes.Buffer

  • 常见误用:想省一次拷贝而试图从 strings.Builder[]byte,结果发现无接口、无字段、无法安全访问
  • 真正需要零拷贝字节操作(如协议编码、base64 写入、加密上下文)—— 必须用 bytes.Buffer 或直接管理 []byte
  • strings.BuilderGrow(n) 是提示,不是保证;bytes.BufferGrow(n) 同样只是建议,但它的 Bytes() 在扩容后会返回新底层数组,旧引用立即失效

性能差异在高频小拼接中才明显

单次拼接几十个字符串,两者差距几乎不可测;但在循环内每轮拼接上百次、持续数万轮的场景下,strings.Builder 因更少的方法调用、无接口动态分发、无读取逻辑,实测快 10%–25%,GC 压力也略低。

立即学习“go语言免费学习笔记(深入)”;

  • 基准测试时注意:不要只测 String() 时间,要包含整个生命周期(构造 → 写入 → 结果使用)
  • 如果拼接后立刻转成 []byte(如 []byte(b.String())),那 bytes.Bufferb.Bytes() 可能反而更快(避免 string → []byte 二次分配)
  • 交叉使用场景(比如先 Builder 拼主干,再 Buffer 补充二进制头)不如统一用 bytes.Buffer —— 混用增加心智负担,且没实际收益

真正的取舍点不在性能数字,而在「你是否需要 Builder 所拒绝提供的能力」。只要没用到 ReadWriteByteReset 或中间读取,就用 strings.Builder;一旦出现这些需求,切换过去并不贵,但提前选错会埋下隐性维护成本。