1、字符串拼接方法汇总

go语言中提供了多种字符串的拼接方法,我总结了以下几种:

  • 1、++=操作符
  • 2、bytes包的bytes.Buffer
  • 3、strings包的strings.Builder
  • 4、strings包的strings.Join
  • 5、fmt.Sprintf()

2、字符串拼接方法性能测试

go版本: ➜ go go version go version go1.17.2 darwin/amd64

接下来,针对不同的情况,对不同的字符串拼接方法进行性能测试,找出字符串拼接的最优使用方法。
字符串拼接的使用场景分为两类:

  • 1、已知要拼接的字符串长度和次数,可一次性完成字符串的拼接
  • 2、未知要拼接的字符串长度和次数,需要循环追加完成字符串的拼接

以上两类分为三种情况:

  • 1、字符串总长度32字节以下
  • 2、字符串总长度32-64字节
  • 3、字符串总长度64字节以上

2.1、测试已知要拼接的字符串长度和次数,可一次性完成字符串的拼接场景的性能

测试结论:对于已知要拼接的字符串长度和次数,可一次性完成字符串的拼接的场景,直接使用+号即可

  1. // 基准代码:待拼接字符串长度、次数已知,可一次完成字符串拼接
  2. //StrKnownPlus + 拼接
  3. func StrKnownPlus(str [3]string) string {
  4. return str[0] + str[1] + str[2]
  5. }
  6. // StrKnownPlusEq += 拼接
  7. func StrKnownPlusEq(str [3]string) string {
  8. var res string
  9. res += str[0]
  10. res += str[1]
  11. res += str[2]
  12. return res
  13. }
  14. // StrKnownBytesBuffer bytes.Buffer
  15. func StrKnownBytesBuffer(str [3]string) string {
  16. var b bytes.Buffer
  17. b.WriteString(str[0])
  18. b.WriteString(str[1])
  19. b.WriteString(str[2])
  20. return b.String()
  21. }
  22. // StrKnownStringsBuilder strings.Builder
  23. func StrKnownStringsBuilder(str [3]string) string {
  24. var b strings.Builder
  25. b.WriteString(str[0])
  26. b.WriteString(str[1])
  27. b.WriteString(str[2])
  28. return b.String()
  29. }
  30. // StrKnownStringsJoin strings.Join
  31. func StrKnownStringsJoin(str [3]string) string {
  32. return strings.Join(str[:], "")
  33. }
  34. // StrKnownFmtSprintf fmt.Sprintf
  35. func StrKnownFmtSprintf(str [3]string) string {
  36. return fmt.Sprintf("%s%s%s", str[0], str[1], str[2])
  37. }

分别以32字节,32-64字节,大于64字节进行性能测试:

2.1.1、32字节以下

结论:+号性能明显,且未进行内存分配

  1. go go test -bench=. -run=none -benchmem
  2. 字符串数组: [哈哈哈 嘿嘿嘿 哼哼哼]
  3. 字符串长度: 27
  4. goos: darwin
  5. goarch: amd64
  6. pkg: mygo
  7. cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  8. BenchmarkStrKnownPlus-8 50122389 23.72 ns/op 0 B/op 0 allocs/op
  9. BenchmarkStrKnownPlusEq-8 12904890 85.75 ns/op 56 B/op 2 allocs/op
  10. BenchmarkStrKnownBytesBuffer-8 15997658 68.17 ns/op 96 B/op 2 allocs/op
  11. BenchmarkStrKnownStringsBuilder-8 17098168 65.40 ns/op 48 B/op 2 allocs/op
  12. BenchmarkStrKnownStringsJoin-8 23156059 48.63 ns/op 32 B/op 1 allocs/op
  13. BenchmarkStrKnownFmtSprintf-8 6200985 184.2 ns/op 80 B/op 4 allocs/op
  14. PASS
  15. ok mygo 7.302s
  16. * 结论:+号性能明显,且未进行内存分配

2.1.2、32-64字节

结论:+号和strings.Join性能相近,且都只进行了1次内存分配 此时bytes.Buffer性能优于strings.Builder

  1. go go test -bench=. -run=none -benchmem
  2. 字符串数组: [哈哈哈哈哈哈 嘿嘿嘿嘿嘿嘿 哼哼哼哼哼哼]
  3. 字符串长度: 54
  4. goos: darwin
  5. goarch: amd64
  6. pkg: mygo
  7. cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  8. BenchmarkStrKnownPlus-8 22921731 52.40 ns/op 64 B/op 1 allocs/op
  9. BenchmarkStrKnownPlusEq-8 11653983 99.43 ns/op 112 B/op 2 allocs/op
  10. BenchmarkStrKnownBytesBuffer-8 14588312 77.13 ns/op 128 B/op 2 allocs/op
  11. BenchmarkStrKnownStringsBuilder-8 9932362 112.9 ns/op 168 B/op 3 allocs/op
  12. BenchmarkStrKnownStringsJoin-8 20319056 54.23 ns/op 64 B/op 1 allocs/op
  13. BenchmarkStrKnownFmtSprintf-8 6038271 193.3 ns/op 112 B/op 4 allocs/op
  14. PASS
  15. ok mygo 8.504s
  16. * 结论:+号和strings.Join性能相近,且都只进行了1次内存分配
  17. * 此时bytes.Buffer性能优于strings.Builder

2.1.3、大于64字节

结论:+号和strings.Join性能相近,且都只进行了1次内存分配 大于128字节时,strings.Builder的性能开始超过bytes.Buffer

  1. 大于64字节
  2. go go test -bench=. -run=none -benchmem
  3. 字符串数组: [哈哈哈哈哈哈哈哈哈哈哈 嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿 哼哼哼哼哼哼哼哼哼哼哼]
  4. 字符串长度: 99
  5. goos: darwin
  6. goarch: amd64
  7. pkg: mygo
  8. cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  9. BenchmarkStrKnownPlus-8 17173803 58.24 ns/op 112 B/op 1 allocs/op
  10. BenchmarkStrKnownPlusEq-8 9359018 112.1 ns/op 192 B/op 2 allocs/op
  11. BenchmarkStrKnownBytesBuffer-8 8270330 136.1 ns/op 352 B/op 3 allocs/op
  12. BenchmarkStrKnownStringsBuilder-8 8461840 143.8 ns/op 336 B/op 3 allocs/op
  13. BenchmarkStrKnownStringsJoin-8 18779017 61.07 ns/op 112 B/op 1 allocs/op
  14. BenchmarkStrKnownFmtSprintf-8 5816430 200.9 ns/op 160 B/op 4 allocs/op
  15. PASS
  16. ok mygo 8.458s
  17. **************************************************************
  18. 大于128字节
  19. go go test -bench=. -run=none -benchmem
  20. 字符串数组: [哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈 嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿嘿 哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼哼]
  21. 字符串长度: 234
  22. goos: darwin
  23. goarch: amd64
  24. pkg: mygo
  25. cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  26. BenchmarkStrKnownPlus-8 13768717 75.08 ns/op 240 B/op 1 allocs/op
  27. BenchmarkStrKnownPlusEq-8 8419455 138.4 ns/op 400 B/op 2 allocs/op
  28. BenchmarkStrKnownBytesBuffer-8 4245463 289.6 ns/op 1120 B/op 4 allocs/op
  29. BenchmarkStrKnownStringsBuilder-8 6856287 171.4 ns/op 560 B/op 3 allocs/op
  30. BenchmarkStrKnownStringsJoin-8 12730632 79.58 ns/op 240 B/op 1 allocs/op
  31. BenchmarkStrKnownFmtSprintf-8 4830666 237.8 ns/op 288 B/op 4 allocs/op
  32. PASS
  33. ok mygo 7.824s
  34. * 结论:+号和strings.Join性能相近,且都只进行了1次内存分配
  35. * 大于128字节时,strings.Builder的性能开始超过bytes.Buffer

2.2、测试未知要拼接的字符串长度和次数,需要遍历完成字符串的拼接场景的性能

  1. // 待拼接字符串长度、次数未知,需要循环追加完成拼接操作
  2. func StringPlus(n int) {
  3. var res string
  4. for i := 0; i < n; i++ {
  5. res += s1
  6. }
  7. }
  8. func BytesBuffer(n int) {
  9. var res bytes.Buffer
  10. for i := 0; i < n; i++ {
  11. res.WriteString(s1)
  12. }
  13. }
  14. func StringsBuilder(n int) {
  15. var res strings.Builder
  16. for i := 0; i < n; i++ {
  17. res.WriteString(s1)
  18. }
  19. }
  20. func StrStringsJoin(n int) {
  21. var res string
  22. for i := 0; i < n; i++ {
  23. res = strings.Join([]string{res, s1}, "")
  24. }
  25. }
  26. func StrFmtSprintf(n int) {
  27. var res string
  28. for i := 0; i < n; i++ {
  29. res = fmt.Sprintf("%s%s", res, s1)
  30. }
  31. }

与已知的情况相同,分别以32字节,32-64字节,大于64字节进行性能测试:

2.2.1、32字节以下

结论:bytes.Buffer性能优越,+号每次拼接都会生成新的字符串,导致大量的字符串创建、替代

  1. go go test -bench=. -run=none -benchmem
  2. 字符串: hello wd
  3. 字符串长度: 24
  4. goos: darwin
  5. goarch: amd64
  6. pkg: mygo
  7. cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  8. BenchmarkStringPlus-8 12549156 86.71 ns/op 40 B/op 2 allocs/op
  9. BenchmarkBytesBuffer-8 23654611 46.60 ns/op 64 B/op 1 allocs/op
  10. BenchmarkStringsBuilder-8 11896252 93.41 ns/op 56 B/op 3 allocs/op
  11. BenchmarkStringsJoin-8 10319251 114.0 ns/op 48 B/op 3 allocs/op
  12. BenchmarkFmtSprintf-8 2950306 416.4 ns/op 128 B/op 8 allocs/op
  13. PASS
  14. ok mygo 6.500s
  15. * 结论:bytes.Buffer性能优越,+号每次拼接都会生成新的字符串,导致大量的字符串创建、替代

2.2.2、32-64字节

结论:bytes.Buffer性能优越

  1. go go test -bench=. -run=none -benchmem
  2. 字符串: hello wd
  3. 字符串长度: 56
  4. goos: darwin
  5. goarch: amd64
  6. pkg: mygo
  7. cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  8. BenchmarkStringPlus-8 4617710 263.2 ns/op 232 B/op 6 allocs/op
  9. BenchmarkBytesBuffer-8 16271342 65.81 ns/op 64 B/op 1 allocs/op
  10. BenchmarkStringsBuilder-8 8089417 142.6 ns/op 120 B/op 4 allocs/op
  11. BenchmarkStringsJoin-8 4104691 300.1 ns/op 240 B/op 7 allocs/op
  12. BenchmarkFmtSprintf-8 1000000 1107 ns/op 448 B/op 20 allocs/op
  13. PASS
  14. ok mygo 6.589s
  15. * 结论:bytes.Buffer性能优越

2.2.3、大于64字节

结论:bytes.Buffer性能优越

  1. go go test -bench=. -run=none -benchmem
  2. 字符串: hello wd
  3. 字符串长度: 80
  4. goos: darwin
  5. goarch: amd64
  6. pkg: mygo
  7. cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  8. BenchmarkStringPlus-8 3002607 402.9 ns/op 456 B/op 9 allocs/op
  9. BenchmarkBytesBuffer-8 8743602 132.5 ns/op 208 B/op 2 allocs/op
  10. BenchmarkStringsBuilder-8 6029971 196.6 ns/op 248 B/op 5 allocs/op
  11. BenchmarkStringsJoin-8 2704371 439.4 ns/op 464 B/op 10 allocs/op
  12. BenchmarkFmtSprintf-8 784532 1505 ns/op 768 B/op 29 allocs/op
  13. PASS
  14. ok mygo 7.144s
  15. *****************************************************************
  16. go go test -bench=. -run=none -benchmem
  17. 字符串: hello wd
  18. 字符串长度: 160
  19. goos: darwin
  20. goarch: amd64
  21. pkg: mygo
  22. cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  23. BenchmarkStringPlus-8 1222159 1006 ns/op 1736 B/op 19 allocs/op
  24. BenchmarkBytesBuffer-8 4360942 279.1 ns/op 496 B/op 3 allocs/op
  25. BenchmarkStringsBuilder-8 3877113 300.0 ns/op 504 B/op 6 allocs/op
  26. BenchmarkStringsJoin-8 1000000 1008 ns/op 1744 B/op 20 allocs/op
  27. BenchmarkFmtSprintf-8 312130 3260 ns/op 2368 B/op 59 allocs/op
  28. PASS
  29. ok mygo 7.285s
  30. *****************************************************************
  31. go go test -bench=. -run=none -benchmem
  32. 字符串: hello wd
  33. 字符串长度: 400
  34. goos: darwin
  35. goarch: amd64
  36. pkg: mygo
  37. cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  38. BenchmarkStringPlus-8 353895 3406 ns/op 10536 B/op 49 allocs/op
  39. BenchmarkBytesBuffer-8 1918881 633.4 ns/op 1072 B/op 4 allocs/op
  40. BenchmarkStringsBuilder-8 2337927 519.4 ns/op 1016 B/op 7 allocs/op
  41. BenchmarkStringsJoin-8 344072 3539 ns/op 10544 B/op 50 allocs/op
  42. BenchmarkFmtSprintf-8 120430 9246 ns/op 12133 B/op 149 allocs/op
  43. PASS
  44. ok mygo 9.068s
  45. * 结论:总体bytes.Buffer性能优越,但是字符串过长时,strings.Builder的性能才开始凸显

接下来我们使用strings.BuilderGrowbytes.BufferGrow方法试试看:

结论:使用Grow函数后,strings.Builder的性能优于bytes.Buffer

  1. go go test -bench=. -run=none -benchmem
  2. 字符串: hello wd
  3. 字符串长度: 80
  4. goos: darwin
  5. goarch: amd64
  6. pkg: mygo
  7. cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  8. BenchmarkStringPlus-8 2813046 398.9 ns/op 456 B/op 9 allocs/op
  9. BenchmarkBytesBuffer-8 12869452 91.91 ns/op 80 B/op 1 allocs/op
  10. BenchmarkStringsBuilder-8 16718022 70.08 ns/op 80 B/op 1 allocs/op
  11. BenchmarkStringsJoin-8 2641453 440.3 ns/op 464 B/op 10 allocs/op
  12. BenchmarkFmtSprintf-8 760904 1547 ns/op 768 B/op 29 allocs/op
  13. PASS
  14. ok mygo 7.908s
  15. *****************************************************************
  16. go go test -bench=. -run=none -benchmem
  17. 字符串: hello wd
  18. 字符串长度: 400
  19. goos: darwin
  20. goarch: amd64
  21. pkg: mygo
  22. cpu: Intel(R) Core(TM) i5-8257U CPU @ 1.40GHz
  23. BenchmarkStringPlus-8 370012 3243 ns/op 10536 B/op 49 allocs/op
  24. BenchmarkBytesBuffer-8 3490198 334.8 ns/op 416 B/op 1 allocs/op
  25. BenchmarkStringsBuilder-8 4234614 281.9 ns/op 416 B/op 1 allocs/op
  26. BenchmarkStringsJoin-8 354285 3475 ns/op 10544 B/op 50 allocs/op
  27. BenchmarkFmtSprintf-8 121012 9187 ns/op 12132 B/op 149 allocs/op
  28. PASS
  29. ok mygo 8.360s
  30. * 结论:使用Grow函数后,strings.Builder的性能优于bytes.Buffer

3、总结

  • 对于已知要拼接的字符串长度和次数,可一次性完成字符串的拼接的场景,直接使用+号即可;
  • 如果字符串是一个切片要拼接,直接用strings.Join()效率最高
  • 如果是追加方式拼接字符串,使用bytes.Bufferstrings.Builder效率最好

关于+拼接过程:

  • 1、编辑器将字符转成字符数组,调用runtime/string.goconcatstrings()函数
  • 2、遍历数组,获取字符串长度
  • 3、如果字符数组总长度未超过预留buf(32字节),使用预留,反之则根据总长度申请新的内存空间
  • 4、将字符串逐个拷贝到新数组,销毁旧数据

+=+类似。一般在循环中做字符串的追加,每追加一次就会生成一个新的字符串替代旧的,效率低下。

关于bytes.Bufferstrings.Builder

  • 两者都通过创建[]byte,用于缓存需要拼接的字符串
  • 两者都在首次使用WriteString()时进行内存分配
  • 两者都会进行动态扩容,但是策略不太一样

关于strings.Join()

  • 接收一个字符切片,底层使用strings.Builder
  • 获取字符切片的字符的总长度,通过builder.Grow分配内存