在日常编程中,使用fmt.Sprintf可以非常方便的拼接出字符串,但是你知道吗,人畜无害的fmt.Sprintf竟然是一个性能killer。
在日常的coding中,我们一般会使用以下几种方法去拼接字符串
// 1. + 运算符
str := str1+str2
// 2. fmt.Sprintf
str := fmt.Sprintf("%s %s",str1,str2)
// 3. strings.Builder
builder := strings.Builder{}
builder.WirteStirng(str1)
builder.WirteStirng(str2)
str := builder.String()
究竟哪种方法才是最合适的,下面我们将利用benchmark进行性能对比分析
性能分析构造字符串拼接代码
var str1, str2 = "1", "2"
var strList = []string{str1, str2}
// MakeStrWithAdd + 运算符拼接
func MakeStrWithAdd() string {
return str1 + ";" + str2
}
// MakeStrWithFmt fmt拼接
func MakeStrWithFmt() string {
return fmt.Sprintf("%s;%s", str1, str2)
}
// MakeStrWithJoin join拼接
func MakeStrWithJoin() string {
return strings.Join(strList, ";")
}
// MakeStrWithBuilder builder拼接
func MakeStrWithBuilder() string {
var builder strings.Builder
builder.WriteString(str1)
builder.WriteString(str2)
return builder.String()
}
性能分析代码如下
var l = flag.Int("l", 10, "string length")
func TestMain(m *testing.M) {
flag.Parse()
str1 = string(make([]byte,*l))
str2 = string(make([]byte,*l))
strList = []string{str1, str2}
m.Run()
}
func BenchmarkMakeStr(b *testing.B) {
b.ResetTimer()
b.Run("add", func(b *testing.B) {
for i := 0; i < b.N; i++ {
MakeStrWithAdd()
}
})
b.Run("fmt", func(b *testing.B) {
for i := 0; i < b.N; i++ {
MakeStrWithFmt()
}
})
b.Run("join", func(b *testing.B) {
for i := 0; i < b.N; i++ {
MakeStrWithJoin()
}
})
b.Run("builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
MakeStrWithBuilder()
}
})
}
分别测试在字符串不同长度下的benchmark结果
# go test -bench=. -benchmem -l=10 BenchmarkMakeStr/add-12 43891863 23.7 ns/op 0 B/op 0 allocs/op BenchmarkMakeStr/fmt-12 8019594 143 ns/op 64 B/op 3 allocs/op BenchmarkMakeStr/join-12 28029506 41.6 ns/op 32 B/op 1 allocs/op BenchmarkMakeStr/builder-12 18916494 62.5 ns/op 48 B/op 2 allocs/op # go test -bench=. -benchmem -l=100 BenchmarkMakeStr/add-12 18775459 63.5 ns/op 208 B/op 1 allocs/op BenchmarkMakeStr/fmt-12 7085766 168 ns/op 240 B/op 3 allocs/op BenchmarkMakeStr/join-12 17654107 62.0 ns/op 208 B/op 1 allocs/op BenchmarkMakeStr/builder-12 12204345 96.1 ns/op 336 B/op 2 allocs/op # go test -bench=. -benchmem -l=1000 BenchmarkMakeStr/add-12 4781292 265 ns/op 2048 B/op 1 allocs/op BenchmarkMakeStr/fmt-12 3045002 389 ns/op 2081 B/op 3 allocs/op BenchmarkMakeStr/join-12 4464231 258 ns/op 2048 B/op 1 allocs/op BenchmarkMakeStr/builder-12 3017283 369 ns/op 3072 B/op 2 allocs/op # go test -bench=. -benchmem -l=10000 BenchmarkMakeStr/add-12 669583 1627 ns/op 20480 B/op 1 allocs/op BenchmarkMakeStr/fmt-12 534519 2265 ns/op 20587 B/op 3 allocs/op BenchmarkMakeStr/join-12 666103 1742 ns/op 20480 B/op 1 allocs/op BenchmarkMakeStr/builder-12 496591 2558 ns/op 30720 B/op 2 allocs/op
可以看到字符串fmt方式拼接的性能损耗是最大的,而最简单的 +拼接确实最节省性能的。
另外随着字符串长度的变大,+的优势不再那么明显,这个主要是因为性能主要损耗在了内存copy
那么Add就是银弹了吗?下面我们针对字符串数量不固定的场景做下benchmark
修改代码如下,其中fmt针对变长slice不好处理,所以暂不测试。
var list []string
// MakeStrWithAdd2 + 运算符拼接
func MakeStrWithAdd2() (str string) {
switch len(list) {
case 0:
return ""
case 1:
return list[0]
}
for _, item := range list {
str += item
}
return
}
// MakeStrWithJoin2 join拼接
func MakeStrWithJoin2() string {
return strings.Join(list, "")
}
// MakeStrWithBuilder2 builder拼接
func MakeStrWithBuilder2() string {
var builder strings.Builder
for _, item := range list {
builder.WriteString(item)
}
return builder.String()
}
修改benchmark代码如下
var l = flag.Int("l", 10, "string length")
var n = flag.Int("str_number", 2, "string number")
func TestMain(m *testing.M) {
flag.Parse()
str1 = string(make([]byte, *l))
str2 = string(make([]byte, *l))
strList = []string{str1, str2}
list = make([]string, *n)
for i := 0; i < len(list); i++ {
list[i] = "1234567890"
}
m.Run()
}
func BenchmarkMakeStr2(b *testing.B) {
b.ResetTimer()
b.Run("add", func(b *testing.B) {
for i := 0; i < b.N; i++ {
MakeStrWithAdd2()
}
})
b.Run("join", func(b *testing.B) {
for i := 0; i < b.N; i++ {
MakeStrWithJoin2()
}
})
b.Run("builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
MakeStrWithBuilder2()
}
})
}
benchmark如下
# go test -bench=BenchmarkMakeStr2 -benchmem -str_number=2 BenchmarkMakeStr2/add-12 19187944 54.7 ns/op 32 B/op 1 allocs/op BenchmarkMakeStr2/join-12 25535234 46.0 ns/op 32 B/op 1 allocs/op BenchmarkMakeStr2/builder-12 17462570 70.4 ns/op 48 B/op 2 allocs/op # go test -bench=BenchmarkMakeStr2 -benchmem -str_number=10 BenchmarkMakeStr2/add-12 2486616 475 ns/op 608 B/op 9 allocs/op BenchmarkMakeStr2/join-12 8842040 129 ns/op 112 B/op 1 allocs/op BenchmarkMakeStr2/builder-12 6427929 187 ns/op 240 B/op 4 allocs/op # go test -bench=BenchmarkMakeStr2 -benchmem -str_number=20 BenchmarkMakeStr2/add-12 1229839 972 ns/op 2224 B/op 19 allocs/op BenchmarkMakeStr2/join-12 5468655 223 ns/op 208 B/op 1 allocs/op BenchmarkMakeStr2/builder-12 4678641 254 ns/op 496 B/op 5 allocs/op # go test -bench=BenchmarkMakeStr2 -benchmem -str_number=30 BenchmarkMakeStr2/add-12 625074 2057 ns/op 4912 B/op 29 allocs/op BenchmarkMakeStr2/join-12 3677042 323 ns/op 320 B/op 1 allocs/op BenchmarkMakeStr2/builder-12 3243637 367 ns/op 1008 B/op 6 allocs/op
可以看到随着字符串个数的增多,+的性能退化的特别明显,反而使用builder的性能稳定很多。
结论-
针对字符串固定的场景,推荐使用+做字符串拼接,代码简介易实现
-
针对字符串变长的场景,特别是字符串特别多的场景,建议使用builder。
-
另外fmt.Sprintf并不是一无是处,针对占位类型比较多且复杂的场景,如果对性能没有特别苛刻,还是推荐使用的,比较代码简洁也是一种生产力。



