strings.Builder 更适合纯字符串拼接,轻量高效、零值安全、避免内存逃逸;bytes.Buffer 功能更全,支持I/O接口、中间读取和多种写入方式,但有额外开销。
当目标是构建一个最终为 string 的结果,且过程中不涉及二进制数据、不需读取中间状态、也不需要像 io.Reader 那样流转时,strings.Builder 是更轻量、更高效的选择。它底层复用 []byte,但禁止直接访问底层数组,避免了意外修改和内存逃逸。
strings.Builder{} 是零值安全的,无需初始化容量(当然预估长度并调用 Grow() 能进一步减少扩容)string 和 []byte(后者会转成字符串),不支持单个 byte 或 rune —— 如果你需要频繁写入字节或 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 实现了 io.Reader、io.Writer、io.ByteReader、io.RuneScanner 等多个接口,能无缝接入标准库 I/O 流程。如果你需要把拼接过程当作流处理(比如传给 json.Encoder、template.Execute、或做部分读取),它几乎是唯一选择。
byte(WriteByte())、rune(WriteRune())、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() 方法,强行通过反射或 unsafe 获取底层数组属于未定义行为,且 Go 1.20+ 对其内部结构做了调整,极易出错。如果业务逻辑中突然需要「在拼接中途读取原始字节」或「把 Builder 当作 buffer 复用」,说明设计上已偏离它的定位 —— 这时应直接换用 bytes.Buffer。
strings.Builder 拿 []byte,结果发现无接口、无字段、无法安全访问bytes.Buffer 或直接管理 []byte
strings.Builder 的 Grow(n) 是提示,不是保证;bytes.Buffer 的 Grow(n) 同样只是建议,但它的 Bytes() 在扩容后会返回新底层数组,旧引用立即失效单次拼接几十个字符串,两者差距几乎不可测;但在循环内每轮拼接上百次、持续数万轮的场景下,strings.Builder 因更少的方法调用、无接口动态分发、无读取逻辑,实测快 10%–25%,GC 压力也略低。
立即学习“go语言免费学习笔记(深入)”;
String() 时间,要包含整个生命周期(构造 → 写入 → 结果使用)[]byte(如 []byte(b.String())),那 bytes.Buffer 的 b.Bytes() 可能反而更快(避免 string → []byte 二次分配)bytes.Buffer —— 混用增加心智负担,且没实际收益真正的取舍点不在性能数字,而在「你是否需要 Builder 所拒绝提供的能力」。只要没用到 Read、WriteByte、Reset 或中间读取,就用 strings.Builder;一旦
出现这些需求,切换过去并不贵,但提前选错会埋下隐性维护成本。