关于go的字符串追加
通常使用go的字符串追加方式有三种
- 字符串通过+追加
- 字符串通过fmt.Sprintf拼接字符串
- 通过string.Builder+预分配空间追加
// 长度为1000的字符串切片
data = []string{....}
// with fmt
for _, t := range data {
s = fmt.Sprintf("%s%s", s, t)
}
// with append
for _, t := range data {
s += t
}
// string.Builder
var builder strings.Builder
builder.Grow(totalLen) // 预分配
for _, s := range data {
builder.WriteString(s)
}
_ = builder.String()
通过阅读源码可知
fmt.Sprintf过程每次会newPrinter,然后在遍历参数,对每个参数做append操作
普通append会根据新加字符串长度扩容底层rune切片,实现字符串的追加
builder会直接操作底层切片,在预分配后,可减少扩容次数,并更加高效利用内存
基准测试文件
package main
import (
"fmt"
"strings"
"testing"
)
// 生成测试数据
func generateData(n int) []string {
data := make([]string, n)
for i := 0; i < n; i++ {
data[i] = "test-string-"
}
return data
}
// 带预分配的基准测试
func BenchmarkWithPreAlloc(b *testing.B) {
data := generateData(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
totalLen := 0
for _, s := range data {
totalLen += len(s)
}
var builder strings.Builder
builder.Grow(totalLen) // 预分配
for _, s := range data {
builder.WriteString(s)
}
_ = builder.String()
}
}
// 无预分配的基准测试 with append
func BenchmarkWithoutPreAlloc(b *testing.B) {
data := generateData(1000)
b.ResetTimer()
var s string
for i := 0; i < b.N; i++ {
for _, t := range data {
s += t
}
}
}
// 无预分配的基准测试 with fmt
func BenchmarkWithoutPreAllocWithFmt(b *testing.B) {
data := generateData(1000)
b.ResetTimer()
var s string
for i := 0; i < b.N; i++ {
for _, t := range data {
s = fmt.Sprintf("%s%s", s, t)
}
}
}
测试结果
goos: darwin
goarch: arm64
pkg: test
cpu: Apple M2
BenchmarkWithPreAlloc-8 282478 4194 ns/op 12288 B/op 1 allocs/op
BenchmarkWithoutPreAlloc-8 100 29212843 ns/op 604022992 B/op 1040 allocs/op
BenchmarkWithoutPreAllocWithFmt-8 100 73823327 ns/op 1207757819 B/op 4920 allocs/op
PASS
ok test 12.647s
由结果可见,通过string.Builder+预分配模式极大的提高了效率
总结:
底层原理还是通过对切片做了自定义长度,避免了频繁自动扩容造成的时间浪费于内存浪费

浙公网安备 33010602011771号