go字符串拼接不慢,慢在重复创建新字符串;小量用+,动态拼接用strings.builder,格式化优先strconv手动转,预分配容量并避免中间转换可提升性能。

字符串拼接慢?先看你是哪种场景
Go 里字符串拼接不慢,慢的是重复创建新字符串。每次用 + 或 fmt.Sprintf 拼接,都会分配新内存、复制旧内容。如果循环里拼 1000 次,就生成 1000 个中间字符串——这是最常被误踩的坑。
- 小量固定拼接(比如日志模板
"user:" + name + "@" + domain):直接用 +,编译器会优化成一次分配
- 动态多段拼接(比如构建 SQL、HTTP 请求体、JSON 片段):必须用
strings.Builder 或 bytes.Buffer
- 需要格式化(如插入整数、浮点数):优先选
strings.Builder + strconv 手动转,避免 fmt.Fprintf 的反射开销
strings.Builder 和 bytes.Buffer,到底该用谁?
绝大多数情况,选 strings.Builder。它专为字符串拼接设计,零拷贝、无接口调用、内存预分配更友好。
-
strings.Builder 底层用 []byte,但只暴露字符串方法(WriteString、String),不能写二进制数据
-
bytes.Buffer 是通用缓冲区,支持 Write 任意 []byte,但每次 String() 都要检查是否含非法 UTF-8(哪怕你只拼 ASCII)
- 性能差异在百万级拼接时才明显:
Builder 通常快 10%–20%,且 GC 压力更小
- 兼容性注意:
strings.Builder 要求 Go 1.10+;老版本只能用 bytes.Buffer
Builder 不是万能的:这些写法反而拖慢性能strings.Builder 快,前提是别破坏它的内存连续性。
- 别在循环里反复调用
b.Reset() 后重用——不如新建一个,Reset 不清底层数组,但后续 Write 可能触发扩容
- 别用
b.WriteString(fmt.Sprintf(...)):又绕回去了,格式化开销白费
- 预分配容量能省掉多次扩容:
var b strings.Builder; b.Grow(1024),尤其知道最终长度范围时
- 避免频繁调用
b.String():它返回副本,如果只是最后取一次,没问题;但如果循环中每次拼完都取,等于白建 Builder
真实例子:拼接 10 万个 ID,Builder 怎么写才对?
假设你有一批 []int64,想拼成逗号分隔的字符串:
ids := []int64{1, 2, 3, ..., 100000}
var b strings.Builder
b.Grow(2 * len(ids)) // 粗略预估:每个数字最多 20 字节 + 逗号
for i, id := range ids {
if i > 0 {
b.WriteByte(',')
}
strconv.AppendInt(b.AvailableBuffer(), id, 10) // 直接写入底层切片
}
s := b.String()
"user:" + name + "@" + domain):直接用 +,编译器会优化成一次分配strings.Builder 或 bytes.Buffer
strings.Builder + strconv 手动转,避免 fmt.Fprintf 的反射开销strings.Builder。它专为字符串拼接设计,零拷贝、无接口调用、内存预分配更友好。
-
strings.Builder底层用[]byte,但只暴露字符串方法(WriteString、String),不能写二进制数据 -
bytes.Buffer是通用缓冲区,支持Write任意[]byte,但每次String()都要检查是否含非法 UTF-8(哪怕你只拼 ASCII) - 性能差异在百万级拼接时才明显:
Builder通常快 10%–20%,且 GC 压力更小 - 兼容性注意:
strings.Builder要求 Go 1.10+;老版本只能用bytes.Buffer
Builder 不是万能的:这些写法反而拖慢性能strings.Builder 快,前提是别破坏它的内存连续性。
- 别在循环里反复调用
b.Reset() 后重用——不如新建一个,Reset 不清底层数组,但后续 Write 可能触发扩容
- 别用
b.WriteString(fmt.Sprintf(...)):又绕回去了,格式化开销白费
- 预分配容量能省掉多次扩容:
var b strings.Builder; b.Grow(1024),尤其知道最终长度范围时
- 避免频繁调用
b.String():它返回副本,如果只是最后取一次,没问题;但如果循环中每次拼完都取,等于白建 Builder
真实例子:拼接 10 万个 ID,Builder 怎么写才对?
假设你有一批 []int64,想拼成逗号分隔的字符串:
ids := []int64{1, 2, 3, ..., 100000}
var b strings.Builder
b.Grow(2 * len(ids)) // 粗略预估:每个数字最多 20 字节 + 逗号
for i, id := range ids {
if i > 0 {
b.WriteByte(',')
}
strconv.AppendInt(b.AvailableBuffer(), id, 10) // 直接写入底层切片
}
s := b.String()
b.Reset() 后重用——不如新建一个,Reset 不清底层数组,但后续 Write 可能触发扩容b.WriteString(fmt.Sprintf(...)):又绕回去了,格式化开销白费var b strings.Builder; b.Grow(1024),尤其知道最终长度范围时b.String():它返回副本,如果只是最后取一次,没问题;但如果循环中每次拼完都取,等于白建 Builder[]int64,想拼成逗号分隔的字符串:
ids := []int64{1, 2, 3, ..., 100000}
var b strings.Builder
b.Grow(2 * len(ids)) // 粗略预估:每个数字最多 20 字节 + 逗号
for i, id := range ids {
if i > 0 {
b.WriteByte(',')
}
strconv.AppendInt(b.AvailableBuffer(), id, 10) // 直接写入底层切片
}
s := b.String()
关键点:用 strconv.AppendInt 写入 b.AvailableBuffer(),再更新长度,比 b.WriteString(strconv.FormatInt(id, 10)) 少一次内存分配。这个细节很多人忽略,但批量场景下影响显著。
Builder 的高效,藏在“不制造中间字符串”和“可控内存增长”里。一旦开始用 fmt 或 string() 把 byte 切片转来转去,优势就没了。











