Go 语言中提供了基础数据结构类型 string
,在实际使用中我们经常遇到将字符串拼接的问题,即需要将多个字符串拼接在一起,形成新的字符串。那么 go 语言有哪些方式可以完成字符串拼接,以及它们的性能如何,我们一起研究下。
1. 操作符+
最简单的一种方式就是通过操作符+
来完成拼接,例如下面这段小代码,字符串 s3
就是直接将s1和s2拼接在一起,其值等于foobar
1
2
3
|
s1 := "foo"
s2 := "bar"
s3 := s1 + s2
|
2. 通过strings.join函数
strings
库中的strings.join
函数可以拼接多个字符串,并且还能指定字符串之间的分隔符。
1
2
|
// "" 不指定分隔符
s3 := strings.join(s1, s2, "")
|
3. 通过strings.Builder
strings.Builder
有 WriteString
方法,可以直接写入,同时还有Grow
方法,即预先申请内存大小空间
1
2
3
4
5
6
7
|
s1 := "foo"
s2 := "bar"
var str strings.Builder
str.Grow(9)
_, _ = str.WriteString(s1)
_, _ = str.WriteString(s2)
_, _ = str.WriteString(s3
|
4. 通过bytes.Buffer
bytes.Buffer
有 WriteString
方法,可以直接写入:
1
2
3
4
5
6
|
s1 := "foo"
s2 := "bar"
var buf bytes.Buffer
_, _ = buf.WriteString(s1)
_, _ = buf.WriteString(s2)
s3 := buf.String
|
5. 通过[]byte切片
像下面这段代码就是通过[]byte
,将s1和s2拼接在一起,并存入s3。
1
2
3
4
5
6
|
s1 := "foo"
s2 := "bar"
data := make([]byte, 0, 0)
data = append(data, s1...)
data = append(data, s2...)
s3 := string(data)
|
接下来,我们针对上面的几种方式写点基准测试,看下各自的性能。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
package string_join
import (
"bytes"
"strings"
"testing"
)
var (
s1 = "foo"
s2 = "bar"
s3 = "hello"
)
func BenchmarkStringJoinWithOperator(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = s1 + s2 + s3
}
}
func BenchmarkStringJoinWithStringsJoin(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = strings.Join([]string{s1, s2, s3}, "")
}
}
func BenchmarkStringJoinWithStringsBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var str strings.Builder
_, _ = str.WriteString(s1)
_, _ = str.WriteString(s2)
_, _ = str.WriteString(s3)
_ = str.String()
}
}
func BenchmarkStringJoinWithStringsBuilderPreAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
var str strings.Builder
str.Grow(11)
_, _ = str.WriteString(s1)
_, _ = str.WriteString(s2)
_, _ = str.WriteString(s3)
_ = str.String()
}
}
func BenchmarkStringJoinWithBytesBuffer(b *testing.B) {
for i := 0; i < b.N; i++ {
var buf bytes.Buffer
_, _ = buf.WriteString(s1)
_, _ = buf.WriteString(s2)
_, _ = buf.WriteString(s3)
_ = buf.String()
}
}
func BenchmarkStringJoinWithByteSlice(b *testing.B) {
for i := 0; i < b.N; i++ {
var bys []byte
bys = append(bys, s1...)
bys = append(bys, s2...)
bys = append(bys, s3...)
_ = string(bys)
}
}
func BenchmarkStringJoinWithByteSlicePreAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
bys := make([]byte, 0, 11)
bys = append(bys, s1...)
bys = append(bys, s2...)
_ = append(bys, s3...)
_ = string(bys)
}
}
|
运行基准测试,其结果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
$ go test -bench .
goos: darwin
goarch: amd64
pkg: gopatterns/string_join
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkStringJoinWithOperator-12 58113481 20.18 ns/op
BenchmarkStringJoinWithStringsJoin-12 27686020 65.90 ns/op
BenchmarkStringJoinWithStringsBuilder-12 21337910 49.25 ns/op
BenchmarkStringJoinWithStringsBuilderPreAlloc-12 39148916 28.72 ns/op
BenchmarkStringJoinWithBytesBuffer-12 26477316 42.35 ns/op
BenchmarkStringJoinWithByteSlice-12 23214884 51.21 ns/op
BenchmarkStringJoinWithByteSlicePreAlloc-12 100000000 10.73 ns/op
PASS
ok gopatterns/string_join 11.076s
|
由此我们可以发现:
- 带有预分配的
byte
切片性能最好
- 其次是操作符+,仅次于byte切片,性能约为前者的一半
strings.join
最差,其他几个都差不多