写点什么

Go 字符串拼接最佳实践

用户头像
Rayjun
关注
发布于: 3 小时前
Go字符串拼接最佳实践

字符串是一个常见的数据类型,在 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: darwingoarch: amd64pkg: zxin.com/zx-demo/string_benchmarkBenchmarkPlus-12                      12          96586447 ns/op        1086401355 B/op    10057 allocs/opBenchmarkSPrintf-12                   12          97037216 ns/op        1086402698 B/op    10065 allocs/opBenchmarkStringBuilder-12            655           1713353 ns/op        11671537 B/op         35 allocs/opPASSok      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_benchmarkBenchmarkPlus-12                              12          96676019 ns/op        1086401676 B/op    10057 allocs/opBenchmarkSPrintf-12                           12          96693407 ns/op        1086402022 B/op    10058 allocs/opBenchmarkStringBuilder-12                    607           1822282 ns/op        11671543 B/op         35 allocs/opBenchmarkStringBuilderPre-12                 860           1393689 ns/op         8257539 B/op          5 allocs/op
复制代码


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


文 / Rayjun

发布于: 3 小时前阅读数: 3
用户头像

Rayjun

关注

程序员,王小波死忠粉 2017.10.17 加入

非著名程序员,还在学习如何写代码,公众号同名

评论

发布
暂无评论
Go字符串拼接最佳实践