字符串拼接效率怎么提升?strings.Builder 为什么快?
一句话结论:少量拼接用 + 没问题;高频或循环拼接用 strings.Builder:它内部维护 []byte,WriteString 就是 append,最后 String() 零拷贝转成 string,避免不可变字符串反复创建与复制;能预估大小时先 Grow 预分配,把扩容次数降到最低。
各方式对比
| 方式 | 原理 | 特点 | 适用 |
|---|---|---|---|
| + 运算符 | 每次生成新字符串并复制 | 简单直观 | 2~3 个短串 |
| fmt.Sprintf | 反射 + 格式化 | 最慢,有非必要开销 | 需要格式化时 |
| strings.Join | 一次性分配并拼接 | 快,要求统一分隔符 | 切片按分隔符合并 |
| strings.Builder | []byte append,String 零拷贝 | 快,支持 Grow | 循环/高频拼接 |
注意:循环里写 s += x 会让每次迭代都分配一块新内存并复制旧内容,总代价退化为 O(n²)。
为什么 strings.Builder 快
- 底层就是 []byte:写字符串等于 append,可原地扩容,不复制“旧串”。
- Grow(n) 预分配:按最终大小一次扩容,避免多次翻倍搬移。
- String() 用 unsafe 把 []byte 直接转成 string,不做拷贝。
- 代价是返回值与内部缓冲共享底层数组:String() 之后再继续写入可能影响已返回的串,使用上要留意。
// 高吞吐场景的推荐写法
var sb strings.Builder
sb.Grow(estimateLen) // 预估总长度,尽量一次扩容
for _, s := range parts {
sb.WriteString(s)
sb.WriteByte(',')
}
result := sb.String()
sb.Reset() // 循环复用
常见追问 / 记忆点
- 追问:为什么 fmt.Sprintf 慢?——内部走反射解析格式串,还有中间分配;非格式化场景应避免。
- 追问:Builder 适合并发使用吗?——不适合,同实例需自己加锁或每 goroutine 一个。
- 记忆点:高频拼接三连:Builder + Grow 预分配 + Reset 复用;别在循环里用 +。