Go中循环内用+拼接字符串很慢,因每次都会创建新字符串并复制全部内容,时间复杂度O(n)且频繁内存分配;应优先使用strings.Builder,预估容量调用Grow可避免扩容,WriteString均摊O(1)。

为什么 + 拼接在循环里很慢
Go 中用 + 拼接字符串时,每次都会创建新字符串并复制全部内容。字符串底层是只读的 []byte,所以 s1 + s2 实际要分配新底层数组、拷贝 s1 和 s2 的字节——时间复杂度 O(n),且触发频繁内存分配。
循环中反复 +,比如构建日志或 SQL 拼接,性能会随拼接次数线性下降,GC 压力明显上升。
- 单次拼接少量字符串(≤3 个),
+简洁可读,编译器还能做小优化 - 循环内拼接、或拼接数量不确定时,必须避免
+ - 注意:
fmt.Sprintf在循环中同样低效,它内部也依赖+或反射格式化
strings.Builder 是大多数场景的首选
strings.Builder 底层复用 []byte 切片,写入不重新分配,仅在容量不足时扩容(类似 slice 的翻倍策略),WriteString 和 Write 都是 O(1) 均摊复杂度。
它比 bytes.Buffer 更轻量:无锁、无接口转换开销、不支持读操作(设计上就是只写)。
立即学习“go语言免费学习笔记(深入)”;
var b strings.Builder
b.Grow(1024) // 预估容量,减少扩容次数
for _, s := range parts {
b.WriteString(s)
}
result := b.String() // 只在最后调用一次,避免中间转 string
- 务必在循环前调用
b.Grow(n),尤其当知道总长度时,能彻底避免扩容 - 不要在循环中反复调用
b.String(),那会触发底层copy和新分配 - 拼接后若需多次使用
result,Builder本身不能复用(需重置或新建)
小数据量、固定拼接用 fmt.Sprint 或字面量展开
当拼接项少(如 2–4 个)、类型已知、且非高频路径时,fmt.Sprint / fmt.Sprintf 代码清晰,编译器优化后性能差距不大;而纯字面量(如 "prefix" + name + ".txt")会被编译器静态合并为单个字符串常量。
-
fmt.Sprint(a, b, c)比fmt.Sprintf("%s%s%s", a, b, c)稍快(省去格式解析) - 含格式化需求(如数字转字符串带精度)仍用
Sprintf,但避免在 hot path 循环中使用 - 如果所有拼接项都是常量,Go 编译器自动折叠,和手写一个字符串完全等价
需要切片/修改场景考虑 []byte 手动管理
当拼接只是中间步骤,后续还要频繁修改字节(如协议编码、base64 前处理),直接操作 []byte 更高效:避免 string → []byte → string 的反复转换,也绕过字符串不可变约束。
buf := make([]byte, 0, 1024) buf = append(buf, "HTTP/1.1 "...) buf = append(buf, statusBytes...) buf = append(buf, "\r\n"...) // 最后需要 string?只在真正需要时转:string(buf)
- 用
append+ 字面量...语法,比copy更简洁安全 - 注意:
string(buf)会拷贝一份,如果后续只需读,且 buf 生命周期可控,可考虑传buf而非转 string - 此方式丧失字符串语义(如 Unicode 安全遍历),仅适用于明确控制字节流的场景
真正影响性能的往往不是“选哪个 API”,而是是否预估了容量、是否把拼接逻辑错误地塞进了 tight loop、以及是否混淆了「一次性构造」和「流式构建」的模式。Builder 不是银弹,但它覆盖了 80% 的通用拼接需求;剩下的 20%,得看你在拼什么、之后怎么用。











