总结:

如果是次数较少字符串拼接

3次以下是可能是加号最快

如果是多次字符串拼接

  1. 拼接字符串使用 “+ ”和fmt.Sprintf,效率最低,内存占用最大。另外三种类似,极大的优于上面两种,[]byte预分配内存的方式第二快,string.Builder预分配内存的方式第一快。

  2. go官方推荐使用strings.Builder,原因是:

image.png
string.Builder 也提供了预分配内存的方式 Grow:

  1. func builderConcat(n int, str string) string {
  2. var builder strings.Builder
  3. builder.Grow(n * len(str))
  4. for i := 0; i < n; i++ {
  5. builder.WriteString(str)
  6. }
  7. return builder.String()
  8. }

go语言中的字符串拼接一共五种方式:

1.加号

  1. func plusConcat(n int, str string) string {
  2. s := ""
  3. for i := 0; i < n; i++ {
  4. s += str
  5. }
  6. return s
  7. }

2.fmt.Sprintf

  1. func sprintfConcat(n int, str string) string {
  2. s := ""
  3. for i := 0; i < n; i++ {
  4. s = fmt.Sprintf("%s%s", s, str)
  5. }
  6. return s
  7. }

3.strings.Builder

  1. func builderConcat(n int, str string) string {
  2. var builder strings.Builder
  3. for i := 0; i < n; i++ {
  4. builder.WriteString(str)
  5. }
  6. return builder.String()
  7. }

4.bytes.Buffer

  1. func bufferConcat(n int, s string) string {
  2. buf := new(bytes.Buffer)
  3. for i := 0; i < n; i++ {
  4. buf.WriteString(s)
  5. }
  6. return buf.String()
  7. }

5.[]byte

  1. func byteConcat(n int, str string) string {
  2. buf := make([]byte, 0)
  3. for i := 0; i < n; i++ {
  4. buf = append(buf, str...)
  5. }
  6. return string(buf)
  7. }

如果知道字符串长度可以采用预分配:

  1. func preByteConcat(n int, str string) string {
  2. buf := make([]byte, 0, n*len(str))
  3. for i := 0; i < n; i++ {
  4. buf = append(buf, str...)
  5. }
  6. return string(buf)
  7. }

性能比较:
生成了一个长度为 10 的字符串,并拼接 1w 次

  1. func benchmark(b *testing.B, f func(int, string) string) {
  2. var str = randomString(10)
  3. for i := 0; i < b.N; i++ {
  4. f(10000, str)
  5. }
  6. }
  7. func BenchmarkPlusConcat(b *testing.B) { benchmark(b, plusConcat) }
  8. func BenchmarkSprintfConcat(b *testing.B) { benchmark(b, sprintfConcat) }
  9. func BenchmarkBuilderConcat(b *testing.B) { benchmark(b, builderConcat) }
  10. func BenchmarkBufferConcat(b *testing.B) { benchmark(b, bufferConcat) }
  11. func BenchmarkByteConcat(b *testing.B) { benchmark(b, byteConcat) }
  12. func BenchmarkPreByteConcat(b *testing.B) { benchmark(b, preByteConcat) }
  1. $ go test -bench="Concat$" -benchmem .
  2. goos: darwin
  3. goarch: amd64
  4. pkg: example
  5. BenchmarkPlusConcat-8 19 56 ms/op 530 MB/op 10026 allocs/op
  6. BenchmarkSprintfConcat-8 10 112 ms/op 835 MB/op 37435 allocs/op
  7. BenchmarkBuilderConcat-8 8901 0.13 ms/op 0.5 MB/op 23 allocs/op
  8. BenchmarkBufferConcat-8 8130 0.14 ms/op 0.4 MB/op 13 allocs/op
  9. BenchmarkByteConcat-8 8984 0.12 ms/op 0.6 MB/op 24 allocs/op
  10. BenchmarkPreByteConcat-8 17379 0.07 ms/op 0.2 MB/op 2 allocs/op
  11. PASS
  12. ok example 8.627s

2 性能背后的原理

2.1 比较 fmt.Sprintf 和 +

fmt.Sprintf 和 + 性能和内存消耗差距如此巨大,是因为两者的内存分配方式不一样。
字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和。拼接第三个字符串时,再开辟一段新空间,新空间大小是三个字符串大小之和,以此类推。假设一个字符串大小为 10 byte,拼接 1w 次,需要申请的内存大小为:

  1. 10 + 2 * 10 + 3 * 10 + ... + 10000 * 10 byte = 500 MB

strings.Builderbytes.Buffer,包括切片 []byte 的内存是以倍数申请的。例如,初始大小为 0,当第一次写入大小为 10 byte 的字符串时,则会申请大小为 16 byte 的内存(恰好大于 10 byte 的 2 的指数),第二次写入 10 byte 时,内存不够,则申请 32 byte 的内存,第三次写入内存足够,则不申请新的,以此类推。在实际过程中,超过一定大小,比如 2048 byte 后,申请策略上会有些许调整。我们可以通过打印 builder.Cap() 查看字符串拼接过程中,strings.Builder 的内存申请过程。

  1. func TestBuilderConcat(t *testing.T) {
  2. var str = randomString(10)
  3. var builder strings.Builder
  4. cap := 0
  5. for i := 0; i < 10000; i++ {
  6. if builder.Cap() != cap {
  7. fmt.Print(builder.Cap(), " ")
  8. cap = builder.Cap()
  9. }
  10. builder.WriteString(str)
  11. }
  12. }
  13. 运行结果如下:
  14. $ go test -run="TestBuilderConcat" . -v
  15. === RUN TestBuilderConcat
  16. 16 32 64 128 256 512 1024 2048 2688 3456 4864 6144 8192 10240 13568 18432 24576 32768 40960 57344 73728 98304 122880 --- PASS: TestBuilderConcat (0.00s)
  17. PASS
  18. ok example 0.007s

我们可以看到,2048 以前按倍数申请,2048 之后,以 640 递增,最后一次递增 24576 到 122880。总共申请的内存大小约 0.52 MB,约为上一种方式的千分之一。

  1. 16 + 32 + 64 + ... + 122880 = 0.52 MB

2.2 比较 strings.Builder 和 bytes.Buffer

strings.Builderbytes.Buffer 底层都是 []byte 数组,但 strings.Builder 性能比 bytes.Buffer 略快约 10% 。一个比较重要的区别在于,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 []byte 转换成了字符串类型返回了回来。

bytes.Buffer

  1. // To build strings more efficiently, see the strings.Builder type.
  2. func (b *Buffer) String() string {
  3. if b == nil {
  4. // Special case, useful in debugging.
  5. return "<nil>"
  6. }
  7. return string(b.buf[b.off:])
  8. }

strings.Builder

  1. // String returns the accumulated string.
  2. func (b *Builder) String() string {
  3. return *(*string)(unsafe.Pointer(&b.buf))
  4. }

bytes.Buffer 的注释中还特意提到了:

To build strings more efficiently, see the strings.Builder type. 来源: https://geektutu.com/post/hpg-string-concat.html