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.Builder
、bytes.Buffer
和byte[]
,这几种不适合行内使用。当待拼接字符串数量较多时可考虑使用。
先看下其性能测试的对比。
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.Builder
、bytes.Buffer
和byte[]
的性能相近。如果结果字符串的长度是可预知的,使用 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