预估容量比用不用buffer更重要,因默认64字节初始容量拼2mb json会扩容17次、拷贝超30mb;已知总量则预分配make([]byte,0,total),仅知量级可设cap=4mb。

直接用 bytes.Buffer 拼接字符串本身不慢,慢的是没预估容量、乱调 String()、或在并发里瞎写。
为什么预估容量比“用不用 Buffer”更重要
默认 bytes.Buffer 初始底层数组只有 64 字节。拼一个 2MB 的 JSON?会触发约 17 次扩容 —— 每次都 copy 原数据到新切片,实际拷贝量可能超 30MB。
- 已知总长(比如日志模板 + N 个固定长度字段):先算静态部分长度 + 所有变量最大长度之和,传给
bytes.NewBuffer(make([]byte, 0, total)) - 不确定总量但知道量级(如“几 MB”):设
cap = 4 (4MB),比默认 64 强几个数量级 - 循环前遍历一次求和:如果数据源是
[]string或[]byte,先total += len(s)再Grow(total),别省这一步
String() 是性能黑洞,不是语法糖
buf.String() 每次都做一次完整内存拷贝:把底层 []byte 复制成新 string。拼完立刻转 string?等于白干。
- 要写文件或 HTTP 响应:直接
buf.WriteTo(w),零拷贝流式输出 - 只读且不保留引用:用
buf.String()安全,但确认这是最终读取 - 要传给 syscall 或
os.File.Write:用buf.Bytes(),但之后不能再写入 —— 否则切片内容会被覆盖 - 必须保留副本又得继续写:
copy(dst, buf.Bytes()),再buf.Reset()
并发写入 bytes.Buffer 就是自找麻烦
bytes.Buffer 不是线程安全的。两个 goroutine 同时 WriteString,轻则数据错乱,重则 panic(底层切片扩容时触发 data race)。
- 每个 goroutine 自己 new 一个
bytes.Buffer,别共用 - 用
sync.Pool复用?必须buf := pool.Get().(*bytes.Buffer); buf.Reset(),否则残留数据混入新结果 - 嵌入结构体当字段?
Reset()不会自动调用,必须显式管理生命周期
真正容易被忽略的,是 Bytes() 返回的切片和后续写入的冲突 —— 它不是快照,是活指针;还有 Grow() 只保证“至少预留”,不等于“刚好够用”,预估不足照样 realloc。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











