1.行内字符串拼接

行内拼接字符串为了书写方便快捷,最常用的两个方法是:

  • 运算符+
  • fmt.Sprintf()

行内字符串的拼接,主要追求的是代码的简洁可读。fmt.Sprintf() 能够接收不同类型的入参,通过格式化输出完成字符串的拼接,使用非常方便。但因其底层实现使用了反射,性能上会有所损耗。

运算符 + 只能简单地完成字符串之间的拼接,非字符串类型的变量需要单独做类型转换。行内拼接字符串不会产生内存分配,也不涉及类型地动态转换,所以性能上优于fmt.Sprintf()

从性能出发,兼顾实现的便捷与代码可读性,如果待拼接的字符串不涉及类型转换且数量较少(<=3),行内拼接字符串推荐使用运算符 +,反之使用 fmt.Sprintf()

下面看下二者的性能对比。

// Good
func BenchmarkJoinStrWithOperator(b *testing.B) {
    s1, s2, s3 := "foo", "bar", "baz"
    for i := 0; i < b.N; i++ {
        _ = s1 + s2 + s3
    }
}

// Bad
func BenchmarkJoinStrWithSprintf(b *testing.B) {
    s1, s2, s3 := "foo", "bar", "baz"
    for i := 0; i < b.N; i++ {
        _ = fmt.Sprintf("%s%s%s", s1, s2, s3)
    }
}

执行基准测试结果如下:

go test -bench=^BenchmarkJoinStr -benchmem .
BenchmarkJoinStrWithOperator-8    70638928    17.53 ns/op     0 B/op    0 allocs/op
BenchmarkJoinStrWithSprintf-8      7520017    157.2 ns/op    64 B/op    4 allocs/op

可以看到,二者的性能差距很大,快达到一个数量级了。所以在不影响代码可读性时,行内字符串尽可能地使用运算符 + 来拼接而不是fmt.Sprintf()

2.非行内字符串拼接

字符串拼接还有其他的方式,比如strings.Join()strings.Builderbytes.Bufferbyte[],这几种不适合行内使用。当待拼接字符串数量较多时可考虑使用。

先看下其性能测试的对比。

func BenchmarkJoinStrWithStringsJoin(b *testing.B) {
    s1, s2, s3 := "foo", "bar", "baz"
    for i := 0; i < b.N; i++ {
        _ = strings.Join([]string{s1, s2, s3}, "")
    }
}

func BenchmarkJoinStrWithStringsBuilder(b *testing.B) {
    s1, s2, s3 := "foo", "bar", "baz"
    for i := 0; i < b.N; i++ {
        var builder strings.Builder
        _, _ = builder.WriteString(s1)
        _, _ = builder.WriteString(s2)
        _, _ = builder.WriteString(s3)
    }
}

func BenchmarkJoinStrWithBytesBuffer(b *testing.B) {
    s1, s2, s3 := "foo", "bar", "baz"
    for i := 0; i < b.N; i++ {
        var buffer bytes.Buffer
        _, _ = buffer.WriteString(s1)
        _, _ = buffer.WriteString(s2)
        _, _ = buffer.WriteString(s3)
    }
}

func BenchmarkJoinStrWithByteSlice(b *testing.B) {
    s1, s2, s3 := "foo", "bar", "baz"
    for i := 0; i < b.N; i++ {
        var bys []byte
        bys= append(bys, s1...)
        bys= append(bys, s2...)
        _ = append(bys, s3...)
    }
}

func BenchmarkJoinStrWithByteSlicePreAlloc(b *testing.B) {
    s1, s2, s3 := "foo", "bar", "baz"
    for i := 0; i < b.N; i++ {
        bys:= make([]byte, 0, 9)
        bys= append(bys, s1...)
        bys= append(bys, s2...)
        _ = append(bys, s3...)
    }
}

基准测试结果如下:

go test -bench=^BenchmarkJoinStr .
goos: windows
goarch: amd64
pkg: main/perf
cpu: Intel(R) Core(TM) i7-9700 CPU @ 3.00GHz
BenchmarkJoinStrWithStringsJoin-8               31543916                36.39 ns/op
BenchmarkJoinStrWithStringsBuilder-8            30079785                40.60 ns/op
BenchmarkJoinStrWithBytesBuffer-8               31663521                39.58 ns/op
BenchmarkJoinStrWithByteSlice-8                 30748495                37.34 ns/op
BenchmarkJoinStrWithByteSlicePreAlloc-8         665341896               1.813 ns/op

从结果可以看出,strings.Join()strings.Builderbytes.Bufferbyte[] 的性能相近。如果结果字符串的长度是可预知的,使用 byte[] 且预先分配容量的拼接方式性能最佳。

所以如果对性能要求非常严格,或待拼接的字符串数量足够多时,建议使用 byte[] 预先分配容量这种方式。

综合易用性和性能,一般推荐使用strings.Builder来拼接字符串。

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

func BenchmarkJoinStrWithStringsBuilderPreAlloc(b *testing.B) {
    s1, s2, s3 := "foo", "bar", "baz"
    for i := 0; i < b.N; i++ {
        var builder strings.Builder
        builder.Grow(9)
        _, _ = builder.WriteString(s1)
        _, _ = builder.WriteString(s2)
        _, _ = builder.WriteString(s3)
    }
}

使用了 Grow 优化后的版本的性能测试结果如下。可以看出相较于不预先分配空间的方式,性能提升了很多。

BenchmarkJoinStrWithStringsBuilderPreAlloc-8    60079003                20.95 ns/op
powered by Gitbook该文章修订时间: 2024-03-22 15:30:00

results matching ""

    No results matching ""