strings.builder.string() 是零拷贝,bytes.buffer.string() 必须检查 utf-8;builder 直接 unsafe.string() 构造,buffer 遍历字节验证 utf-8,导致后者在高频拼接中慢约 25%。

strings.Builder.String() 是零拷贝,bytes.Buffer.String() 必须检查 UTF-8
这是性能差异最核心的一点:当你调用 strings.Builder.String(),它直接用 unsafe.String() 构造字符串头,不复制底层数组;而 bytes.Buffer.String() 会遍历整个字节切片,逐字节检查是否为合法 UTF-8 —— 即使你只拼了 ASCII 字符(比如 "key=val&"),这一步也逃不掉。
常见错误是以为「反正都是字符串,选哪个都一样」,结果在高频日志拼接或模板渲染中,bytes.Buffer 多出的遍历开销被放大成可观延迟。
- 实测 10 万次单字段拼接(每段约 20 字节),
strings.Builder比bytes.Buffer快约 25% - 若拼接内容含非 UTF-8 字节(如 raw binary),
strings.Builder.WriteString()编译就报错:cannot use []byte{...} as string,根本走不到运行时检查这步 - 如果你后续要
json.Unmarshal([]byte(s)),说明原始数据本就不保证 UTF-8,那必须用bytes.Buffer,别强转
写入路径:Builder 是直调,Buffer 走 io.Writer 接口间接调用
strings.Builder.WriteString() 是普通方法调用,无虚表跳转;bytes.Buffer.WriteString() 是 io.Writer 接口实现,每次都要查接口表——哪怕你只写字符串,也绕不开这一层间接成本。
容易踩的坑是「我只写字符串,又不读,用 Buffer 应该没问题」,但实际多出的指令跳转和寄存器保存,在 tight loop 中累积起来就是可观开销。
- 拼接场景固定(如生成 SQL 查询语句、HTTP header 字符串)→ 用
strings.Builder - 拼完立刻传给
http.ServeContent(w, ...)或json.NewEncoder(w).Encode()→ 必须用bytes.Buffer,因为这些函数只接受io.Writer - 混用场景(先拼字符串,再写入文件)→ 宁可多一次
.String()+io.WriteString(w, s),也别让bytes.Buffer承担纯字符串拼接任务
Grow() 预分配比类型选择影响更大
不调 Grow() 的 strings.Builder,可能比调了 Grow() 的 bytes.Buffer 还慢。两者扩容策略相似(当前 cap × 2 + 需求),但初始容量不同:strings.Builder{} 初始 cap 为 0,第一次写 1 字节就扩到 8;bytes.Buffer{} 默认 cap 64,稍友好些,但依然不够。
真正卡性能的不是类型,而是频繁 realloc 导致的底层数组复制和 GC 压力。
- 能预估总长(如 JSON 对象字段数 × 平均键值长度 + 分隔符)→ 一定调
b.Grow(estimatedTotalLen) - 拼接段数固定(如 3 个字段拼成
"a=1&b=2&c=3")→b.Grow(len(a)+len(b)+len(c)+4)(+4 是两个&和等号) - 完全无法预估 → 宁可高估 2×,也别依赖默认扩容;实测高估 50% 通常比默认扩容快 30%+
复用 Builder 实例的 Reset() 不等于清空内存
strings.Builder.Reset() 只把 len 归零,不释放底层数组。如果上一轮拼了 1MB,下一轮只拼 10 字节,那个 1MB 数组还挂着,既浪费内存,又拖慢 GC。
这不是 bug,是设计使然 —— Go 团队明确说过:复用是为了避免 new,不是为了省内存。
- 循环内固定长度拼接(如批量生成固定格式日志)→ 复用 +
Reset()合理 - 循环内拼接长度波动极大(如用户输入动态组装 HTML)→ 每次新建
strings.Builder{}更稳 - 误以为
Reset()等价于「重置为初始状态」→ 实际仍保留旧 cap,下次WriteString()若超限,照样扩容
io.Writer,或者混着读写。类型只是工具,路径决定开销。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











