1、字符串拼接方法汇总
go语言中提供了多种字符串的拼接方法,我总结了以下几种:
- 1、
+、+=操作符 - 2、
bytes包的bytes.Buffer - 3、
strings包的strings.Builder - 4、
strings包的strings.Join - 5、
fmt.Sprintf()
2、字符串拼接方法性能测试
go版本:➜ go go versiongo version go1.17.2 darwin/amd64
接下来,针对不同的情况,对不同的字符串拼接方法进行性能测试,找出字符串拼接的最优使用方法。
字符串拼接的使用场景分为两类:
- 1、已知要拼接的字符串长度和次数,可一次性完成字符串的拼接
- 2、未知要拼接的字符串长度和次数,需要循环追加完成字符串的拼接
以上两类分为三种情况:
- 1、字符串总长度
32字节以下 - 2、字符串总长度
32-64字节 - 3、字符串总长度
64字节以上
2.1、测试已知要拼接的字符串长度和次数,可一次性完成字符串的拼接场景的性能
测试结论:对于已知要拼接的字符串长度和次数,可一次性完成字符串的拼接的场景,直接使用
+号即可
// 基准代码:待拼接字符串长度、次数已知,可一次完成字符串拼接//StrKnownPlus + 拼接func StrKnownPlus(str [3]string) string {return str[0] + str[1] + str[2]}// StrKnownPlusEq += 拼接func StrKnownPlusEq(str [3]string) string {var res stringres += str[0]res += str[1]res += str[2]return res}// StrKnownBytesBuffer bytes.Bufferfunc StrKnownBytesBuffer(str [3]string) string {var b bytes.Bufferb.WriteString(str[0])b.WriteString(str[1])b.WriteString(str[2])return b.String()}// StrKnownStringsBuilder strings.Builderfunc StrKnownStringsBuilder(str [3]string) string {var b strings.Builderb.WriteString(str[0])b.WriteString(str[1])b.WriteString(str[2])return b.String()}// StrKnownStringsJoin strings.Joinfunc StrKnownStringsJoin(str [3]string) string {return strings.Join(str[:], "")}// StrKnownFmtSprintf fmt.Sprintffunc StrKnownFmtSprintf(str [3]string) string {return fmt.Sprintf("%s%s%s", str[0], str[1], str[2])}
2.1.1、32字节以下
结论:
+号性能明显,且未进行内存分配
➜ go go test -bench=. -run=none -benchmem字符串数组: [哈哈哈 嘿嘿嘿 哼哼哼]字符串长度: 27goos: darwingoarch: amd64pkg: mygocpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmarkStrKnownPlus-8 50122389 23.72 ns/op 0 B/op 0 allocs/opBenchmarkStrKnownPlusEq-8 12904890 85.75 ns/op 56 B/op 2 allocs/opBenchmarkStrKnownBytesBuffer-8 15997658 68.17 ns/op 96 B/op 2 allocs/opBenchmarkStrKnownStringsBuilder-8 17098168 65.40 ns/op 48 B/op 2 allocs/opBenchmarkStrKnownStringsJoin-8 23156059 48.63 ns/op 32 B/op 1 allocs/opBenchmarkStrKnownFmtSprintf-8 6200985 184.2 ns/op 80 B/op 4 allocs/opPASSok mygo 7.302s* 结论:+号性能明显,且未进行内存分配
2.1.2、32-64字节
结论:
+号和strings.Join性能相近,且都只进行了1次内存分配 此时bytes.Buffer性能优于strings.Builder
➜ go go test -bench=. -run=none -benchmem字符串数组: [哈哈哈哈哈哈 嘿嘿嘿嘿嘿嘿 哼哼哼哼哼哼]字符串长度: 54goos: darwingoarch: amd64pkg: mygocpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmarkStrKnownPlus-8 22921731 52.40 ns/op 64 B/op 1 allocs/opBenchmarkStrKnownPlusEq-8 11653983 99.43 ns/op 112 B/op 2 allocs/opBenchmarkStrKnownBytesBuffer-8 14588312 77.13 ns/op 128 B/op 2 allocs/opBenchmarkStrKnownStringsBuilder-8 9932362 112.9 ns/op 168 B/op 3 allocs/opBenchmarkStrKnownStringsJoin-8 20319056 54.23 ns/op 64 B/op 1 allocs/opBenchmarkStrKnownFmtSprintf-8 6038271 193.3 ns/op 112 B/op 4 allocs/opPASSok mygo 8.504s* 结论:+号和strings.Join性能相近,且都只进行了1次内存分配* 此时bytes.Buffer性能优于strings.Builder
2.1.3、大于64字节
结论:
+号和strings.Join性能相近,且都只进行了1次内存分配 大于128字节时,strings.Builder的性能开始超过bytes.Buffer
大于64字节➜ go go test -bench=. -run=none -benchmem字符串数组: [哈哈哈哈哈哈哈哈哈哈哈 嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿 哼哼哼哼哼哼哼哼哼哼哼]字符串长度: 99goos: darwingoarch: amd64pkg: mygocpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmarkStrKnownPlus-8 17173803 58.24 ns/op 112 B/op 1 allocs/opBenchmarkStrKnownPlusEq-8 9359018 112.1 ns/op 192 B/op 2 allocs/opBenchmarkStrKnownBytesBuffer-8 8270330 136.1 ns/op 352 B/op 3 allocs/opBenchmarkStrKnownStringsBuilder-8 8461840 143.8 ns/op 336 B/op 3 allocs/opBenchmarkStrKnownStringsJoin-8 18779017 61.07 ns/op 112 B/op 1 allocs/opBenchmarkStrKnownFmtSprintf-8 5816430 200.9 ns/op 160 B/op 4 allocs/opPASSok mygo 8.458s**************************************************************大于128字节➜ go go test -bench=. -run=none -benchmem字符串数组: [哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈 嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿 哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼]字符串长度: 234goos: darwingoarch: amd64pkg: mygocpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmarkStrKnownPlus-8 13768717 75.08 ns/op 240 B/op 1 allocs/opBenchmarkStrKnownPlusEq-8 8419455 138.4 ns/op 400 B/op 2 allocs/opBenchmarkStrKnownBytesBuffer-8 4245463 289.6 ns/op 1120 B/op 4 allocs/opBenchmarkStrKnownStringsBuilder-8 6856287 171.4 ns/op 560 B/op 3 allocs/opBenchmarkStrKnownStringsJoin-8 12730632 79.58 ns/op 240 B/op 1 allocs/opBenchmarkStrKnownFmtSprintf-8 4830666 237.8 ns/op 288 B/op 4 allocs/opPASSok mygo 7.824s* 结论:+号和strings.Join性能相近,且都只进行了1次内存分配* 大于128字节时,strings.Builder的性能开始超过bytes.Buffer
2.2、测试未知要拼接的字符串长度和次数,需要遍历完成字符串的拼接场景的性能
// 待拼接字符串长度、次数未知,需要循环追加完成拼接操作func StringPlus(n int) {var res stringfor i := 0; i < n; i++ {res += s1}}func BytesBuffer(n int) {var res bytes.Bufferfor i := 0; i < n; i++ {res.WriteString(s1)}}func StringsBuilder(n int) {var res strings.Builderfor i := 0; i < n; i++ {res.WriteString(s1)}}func StrStringsJoin(n int) {var res stringfor i := 0; i < n; i++ {res = strings.Join([]string{res, s1}, "")}}func StrFmtSprintf(n int) {var res stringfor i := 0; i < n; i++ {res = fmt.Sprintf("%s%s", res, s1)}}
与已知的情况相同,分别以32字节,32-64字节,大于64字节进行性能测试:
2.2.1、32字节以下
结论:
bytes.Buffer性能优越,+号每次拼接都会生成新的字符串,导致大量的字符串创建、替代
➜ go go test -bench=. -run=none -benchmem字符串: hello wd字符串长度: 24goos: darwingoarch: amd64pkg: mygocpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmarkStringPlus-8 12549156 86.71 ns/op 40 B/op 2 allocs/opBenchmarkBytesBuffer-8 23654611 46.60 ns/op 64 B/op 1 allocs/opBenchmarkStringsBuilder-8 11896252 93.41 ns/op 56 B/op 3 allocs/opBenchmarkStringsJoin-8 10319251 114.0 ns/op 48 B/op 3 allocs/opBenchmarkFmtSprintf-8 2950306 416.4 ns/op 128 B/op 8 allocs/opPASSok mygo 6.500s* 结论:bytes.Buffer性能优越,+号每次拼接都会生成新的字符串,导致大量的字符串创建、替代
2.2.2、32-64字节
结论:
bytes.Buffer性能优越
➜ go go test -bench=. -run=none -benchmem字符串: hello wd字符串长度: 56goos: darwingoarch: amd64pkg: mygocpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmarkStringPlus-8 4617710 263.2 ns/op 232 B/op 6 allocs/opBenchmarkBytesBuffer-8 16271342 65.81 ns/op 64 B/op 1 allocs/opBenchmarkStringsBuilder-8 8089417 142.6 ns/op 120 B/op 4 allocs/opBenchmarkStringsJoin-8 4104691 300.1 ns/op 240 B/op 7 allocs/opBenchmarkFmtSprintf-8 1000000 1107 ns/op 448 B/op 20 allocs/opPASSok mygo 6.589s* 结论:bytes.Buffer性能优越
2.2.3、大于64字节
结论:
bytes.Buffer性能优越
➜ go go test -bench=. -run=none -benchmem字符串: hello wd字符串长度: 80goos: darwingoarch: amd64pkg: mygocpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmarkStringPlus-8 3002607 402.9 ns/op 456 B/op 9 allocs/opBenchmarkBytesBuffer-8 8743602 132.5 ns/op 208 B/op 2 allocs/opBenchmarkStringsBuilder-8 6029971 196.6 ns/op 248 B/op 5 allocs/opBenchmarkStringsJoin-8 2704371 439.4 ns/op 464 B/op 10 allocs/opBenchmarkFmtSprintf-8 784532 1505 ns/op 768 B/op 29 allocs/opPASSok mygo 7.144s*****************************************************************➜ go go test -bench=. -run=none -benchmem字符串: hello wd字符串长度: 160goos: darwingoarch: amd64pkg: mygocpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmarkStringPlus-8 1222159 1006 ns/op 1736 B/op 19 allocs/opBenchmarkBytesBuffer-8 4360942 279.1 ns/op 496 B/op 3 allocs/opBenchmarkStringsBuilder-8 3877113 300.0 ns/op 504 B/op 6 allocs/opBenchmarkStringsJoin-8 1000000 1008 ns/op 1744 B/op 20 allocs/opBenchmarkFmtSprintf-8 312130 3260 ns/op 2368 B/op 59 allocs/opPASSok mygo 7.285s*****************************************************************➜ go go test -bench=. -run=none -benchmem字符串: hello wd字符串长度: 400goos: darwingoarch: amd64pkg: mygocpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmarkStringPlus-8 353895 3406 ns/op 10536 B/op 49 allocs/opBenchmarkBytesBuffer-8 1918881 633.4 ns/op 1072 B/op 4 allocs/opBenchmarkStringsBuilder-8 2337927 519.4 ns/op 1016 B/op 7 allocs/opBenchmarkStringsJoin-8 344072 3539 ns/op 10544 B/op 50 allocs/opBenchmarkFmtSprintf-8 120430 9246 ns/op 12133 B/op 149 allocs/opPASSok mygo 9.068s* 结论:总体bytes.Buffer性能优越,但是字符串过长时,strings.Builder的性能才开始凸显
接下来我们使用strings.Builder的Grow和bytes.Buffer的Grow方法试试看:
结论:使用
Grow函数后,strings.Builder的性能优于bytes.Buffer
➜ go go test -bench=. -run=none -benchmem字符串: hello wd字符串长度: 80goos: darwingoarch: amd64pkg: mygocpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmarkStringPlus-8 2813046 398.9 ns/op 456 B/op 9 allocs/opBenchmarkBytesBuffer-8 12869452 91.91 ns/op 80 B/op 1 allocs/opBenchmarkStringsBuilder-8 16718022 70.08 ns/op 80 B/op 1 allocs/opBenchmarkStringsJoin-8 2641453 440.3 ns/op 464 B/op 10 allocs/opBenchmarkFmtSprintf-8 760904 1547 ns/op 768 B/op 29 allocs/opPASSok mygo 7.908s*****************************************************************➜ go go test -bench=. -run=none -benchmem字符串: hello wd字符串长度: 400goos: darwingoarch: amd64pkg: mygocpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHzBenchmarkStringPlus-8 370012 3243 ns/op 10536 B/op 49 allocs/opBenchmarkBytesBuffer-8 3490198 334.8 ns/op 416 B/op 1 allocs/opBenchmarkStringsBuilder-8 4234614 281.9 ns/op 416 B/op 1 allocs/opBenchmarkStringsJoin-8 354285 3475 ns/op 10544 B/op 50 allocs/opBenchmarkFmtSprintf-8 121012 9187 ns/op 12132 B/op 149 allocs/opPASSok mygo 8.360s* 结论:使用Grow函数后,strings.Builder的性能优于bytes.Buffer
3、总结
- 对于已知要拼接的字符串长度和次数,可一次性完成字符串的拼接的场景,直接使用
+号即可; - 如果字符串是一个切片要拼接,直接用
strings.Join()效率最高 - 如果是追加方式拼接字符串,使用
bytes.Buffer和strings.Builder效率最好
关于+拼接过程:
- 1、编辑器将字符转成字符数组,调用
runtime/string.go的concatstrings()函数 - 2、遍历数组,获取字符串长度
- 3、如果字符数组总长度未超过预留
buf(32字节),使用预留,反之则根据总长度申请新的内存空间 - 4、将字符串逐个拷贝到新数组,销毁旧数据
+=和+类似。一般在循环中做字符串的追加,每追加一次就会生成一个新的字符串替代旧的,效率低下。
关于bytes.Buffer和strings.Builder:
- 两者都通过创建
[]byte,用于缓存需要拼接的字符串 - 两者都在首次使用
WriteString()时进行内存分配 - 两者都会进行动态扩容,但是策略不太一样
关于strings.Join():
- 接收一个字符切片,底层使用
strings.Builder - 获取字符切片的字符的总长度,通过
builder.Grow分配内存
