go中常见的字符串拼接方法
假设我们现在要实现这样一个拼接函数: 将字符串重复n次拼接起来,返回一个新字符串。
方法一:使用+运算符
func simpleSplice(s string, n int) string {
	newStr := ""
	for i := 0; i < n; i++ {
		newStr += s
	}
	return newStr
}
方法二:使用Sprintf
func sprintfSplice(s string, n int) string {
	newStr := ""
	for i := 0; i < n; i++ {
		newStr = fmt.Sprintf("%s%s", newStr, s)
	}
	return newStr
}
方法三:使用[]byte
func bytesSplice(s string, n int) string {
	newStr := []byte{}
	for i := 0; i < n; i++ {
		newStr = append(newStr, []byte(s)...)
	}
	return string(newStr)
}
方法四:使用bytes.Buffer
func bufferSplice(s string, n int) string {
	buffer := bytes.Buffer{}
	for i := 0; i < n; i++ {
		buffer.WriteString(s)
	}
	return buffer.String()
}
方法五:使用strings.Builder
func builderSplice(s string, n int) string {
	builder := strings.Builder{}
	for i := 0; i < n; i++ {
		builder.WriteString(s)
	}
	return builder.String()
}
性能测试
我们对上面五种方法进行benchmark测试。
测试函数类似这样,生成100长度的随机字符串,然后拼接100次。
func BenchmarkBytesSplice3(b *testing.B) {
	for i := 0; i < b.N; i++ {
		bytesSplice3(genStr(100), 100)
	}
}
结果如下:
BenchmarkSimpleSplice
BenchmarkSimpleSplice-12     	    9901	    123436 ns/op
BenchmarkSprintfSplice
BenchmarkSprintfSplice-12    	    8151	    144824 ns/op
BenchmarkBytesSplice
BenchmarkBytesSplice-12      	   62271	     19435 ns/op
BenchmarkBuilderSplice
BenchmarkBuilderSplice-12    	   93918	     11890 ns/op
BenchmarkBufferSplice
BenchmarkBufferSplice-12     	   97413	     11816 ns/op
以上仅测试了一次, 不要求严谨, 只是为了说明问题
可以看见使用+运算符拼接和Sprintf的性能是最差的。
+运算符/Sprintf
+运算符和Sprintf性能差的原因其实差不多: 每次都会创建一个新的字符串,然后将原来的字符串和新的字符串拼接起来,这样就会产生很多的临时字符串,这些临时字符串会占用很多的内存,而且还会增加GC的负担。
bytes/strings.Builder/buffer
这三者都是利用[]byte实现的功能,所以从当前这个测试中来看差别不大(对比上面那两个来说~)。
我们查看buffer的源码发现, 他每次写入之前会计算好所需的空间, 然后将其copy到[]byte中。
而Builder中虽然使用append追加的数据, 但是使用了unsafe方法直接操作内存指针。
所以操作[]byte来实现拼接是最快, 而buffer和Builder带来的内存优化有多少呢?
内存占用
我们调大需要测试的字符串长度, 这样会更明显: 字符串长度1000, 拼接1000次。
func BenchmarkBytesSplice3(b *testing.B) {
	for i := 0; i < b.N; i++ {
		bytesSplice3(genStr(1000), 1000)
	}
}
运行结果
BenchmarkBytesSplice3-12            2421            521164 ns/op         2033674 B/op       1003 allocs/op
BenchmarkBuilderSplice-12           1135            986065 ns/op         5238292 B/op         26 allocs/op
BenchmarkBufferSplice-12            2733            451784 ns/op         3105806 B/op         14 allocs/op
BytesSplice3为什么会有1003 allocs ? 明明已经预分配内存了
func bytesSplice3(s string, n int) string {
    // 预分配内存   
	newStr := make([]byte, 0, len(s)*n)
	for i := 0; i < n; i++ {
        // 问题在这里, 每次都会创建一个新的[]byte
        // 其实这个动作可以省略掉
		newStr = append(newStr, []byte(s)...)
        // 更换成这种写法试试
        // newStr = append(newStr, s...)
	}
	return *(*string)(unsafe.Pointer(&newStr))
}
更换后:
BenchmarkBytesSplice3-12            8913            135231 ns/op         1009667 B/op          3 allocs/op
BenchmarkBuilderSplice-12           1530            756342 ns/op         5238292 B/op         26 allocs/op
BenchmarkBufferSplice-12            3198            381564 ns/op         3105808 B/op         14 allocs/op
总结
在可预知拼接结果长度的情况下, 使用make([]byte, 0, len(s)*n)这样的方式来预分配内存是最合适的。 其他情况下, 使用strings.Builder, buffer都是可以的。