美文网首页学习
Go 字符串拼接最佳实践

Go 字符串拼接最佳实践

作者: rayjun | 来源:发表于2021-08-21 12:14 被阅读0次

    字符串是一个常见的数据类型,在 Go 语言在内的很多语言中,为了安全,都把字符串设计为不可变。每生成一个字符串都是在创建一个新的字符串,而不是在原有字符串的基础上修改。

    在 Go 中,字符串拼接的方式很多,可以直接使用 +,也可以使用 fmt.SPrintf,还可以使用 strings.Builder 和 bytes.Buffer。

    在这篇文章中,来讨论一下在代码中如何做字符串拼接效率最好。

    1. 做一个基准测试

    在开始分析每种拼接方法的优劣之前,先跑一个简单的基准测试,来看一下每种字符串拼接方法的性能。

    Go 中提供了基准测试框架,测试文件需要以 test 结尾,然后每个测试方法以 Benchmark 开头,这次对加号、fmt.SPrintf、和 strings.Builder 三种方式进行基准测试,代码如下:

    func BenchmarkPlus(b *testing.B) {
        str := "this is just a string"
    
        for i := 0; i < b.N; i++ {
            stringPlus(str)
        }
    }
    
    func BenchmarkSPrintf(b *testing.B) {
        str := "this is just a string"
        for i := 0; i < b.N; i++ {
            stringSprintf(str)
        }
    }
    
    func BenchmarkStringBuilder(b *testing.B) {
        str := "this is just a string"
        for i := 0; i < b.N; i++ {
            stringBuilder(str)
        }
    }
    
    func stringPlus(str string) string {
        s := ""
        for i := 0; i < 10000; i++ {
            s += str
        }
        return s
    }
    
    func stringSprintf(str string) string {
        s := ""
        for i :=0; i < 10000; i++ {
            s += str
        }
        return s
    }
    
    func stringBuilder(str string) string {
        builder := strings.Builder{}
        for i := 0; i < 100000; i++ {
            builder.WriteString(str)
        }
        return builder.String()
    }
    

    基准测试需要使用 *testing.B ,其中 b.N 不是一个固定的值,这个值的大小由框架自己来决定。

    在这里,我们分别测试用不同的方式拼接一个固定的字符串 10000 次,然后统计平均的代码执行时间,内存消耗情况。使用如下的命令运行基准测试:

    go test -bench=. -benchmem
    

    -bench=. 参数运行当前包中所有基准测试,-benchmem 表示对测试的内存使用情况进行统计。运行上面的命令之后,输出结果如下:

    goos: darwin
    goarch: amd64
    pkg: zxin.com/zx-demo/string_benchmark
    BenchmarkPlus-12                      12          96586447 ns/op        1086401355 B/op    10057 allocs/op
    BenchmarkSPrintf-12                   12          97037216 ns/op        1086402698 B/op    10065 allocs/op
    BenchmarkStringBuilder-12            655           1713353 ns/op        11671537 B/op         35 allocs/op
    PASS
    ok      zxin.com/zx-demo/string_benchmark       6.186s
    

    第一列表示基准测试的方法名称和所用的 GOMAXPROCS 的值,第二列表示这次测试循环的次数,第三列表示平均每次测试所用的时间,单位为纳秒,第四列表示平均每次运行所分配的内存,第五列表示每次运行所分配内存的次数。

    通过上面的测试,可以发现 strings.Builder 的表现是最好的,比直接使用加号来拼接字符串的内存消耗要小 100 倍。

    2. 为什么性能的差异这么大

    通过上面的基准测试可以发现,使用不同的方式来拼接字符串,性能差异很大。

    Go 的字符串是不可变的,如果使用加号的方式来拼接字符串,那么每次拼接都需要重新分配内存。而 strings.Builder 会对内存预分配,在字符串不断写入的过程中,会自动扩容长度。

    strings.Builder 的底层存储使用的是 []byte,初始的长度分配是 32,然后每次扩容时都会翻一倍。

    type Builder struct {
        addr *Builder
        buf  []byte
    }
    

    当长度到大 2048 时,再扩容就不会直接翻倍,而是每次增加 640 的倍数,第一次增加 640,第二次增加 1280,以此类推。

    在大量拼接字符串的时候 strings.Builder 会比直接拼接的效率更高。

    bytes.Buffer 是另一个类似的库,与 strings.Builder 性能相当,但如果是对于纯拼接字符串的场景,还是推荐使用 strings.Builder。

    3. 拼字符串的最佳实践

    虽然 strings.Builder 的性能很高,但并不是所有的场景都是合这个。如果只是一次简单的字符串拼接,直接使用加号就够了。

    如果涉及到一些字符串的格式化,那么使用 fmt.Sprintf 就更合适了。

    那么在大量拼接字符串的场景,直接使用 strings.Builder 就完事了么,其实还可以继续优化一下。在使用 strings.Builder 时,如果字符串在不断的增加,底层的存储还是要不断的扩容。如果可以预估字符串的长度,就可以提前分配好内存。减少扩容的次数。

    增加一个测试用例:

    func BenchmarkStringBuilderPre(b *testing.B) {
        str := "this is just a string"
        for i := 0; i < b.N; i++ {
            stringBuilderPre(str)
        }
    }
    
    func stringBuilderPre(str string) string {
        builder := strings.Builder{}
        builder.Grow(1000000)
        for i := 0; i < 100000; i++ {
            builder.WriteString(str)
        }
        return builder.String()
    }
    

    下面是基准测试的结果:

    pkg: zxin.com/zx-demo/string_benchmark
    BenchmarkPlus-12                              12          96676019 ns/op        1086401676 B/op    10057 allocs/op
    BenchmarkSPrintf-12                           12          96693407 ns/op        1086402022 B/op    10058 allocs/op
    BenchmarkStringBuilder-12                    607           1822282 ns/op        11671543 B/op         35 allocs/op
    BenchmarkStringBuilderPre-12                 860           1393689 ns/op         8257539 B/op          5 allocs/op
    

    可以看到,在提前指定长度的情况下,性能又提升了不少,内存的占用量和分配次数下降了不少,运行时间也有所提升。

    文 / Rayjun

    相关文章

      网友评论

        本文标题:Go 字符串拼接最佳实践

        本文链接:https://www.haomeiwen.com/subject/zosfiltx.html