不会飞的章鱼

熟能生巧,勤能补拙;念念不忘,必有回响。

Go语言精进——了解string实现原理并高效使用

Go语言的字符串类型

统一设置为string

1
2
3
4
5
6
7
8
9
10
const (
s = "string constant"
)
func main() {
var s1 string = "string variable"

fmt.Printf("%T\n", s) // string
fmt.Printf("%T\n", s1) // string
fmt.Printf("%T\n", "temporary string literal") // string
}

功能特点:

  • string类型的数据是不可变的;
  • 零值可用;
  • 获取长度的时间复杂度是O(1)级别
  • 支持通过 +/+= 操作符进行字符串连接
  • 支持各种比较关系操作符:==、!=、>=、<=、<、>
  • 对非ASCII字符提供原生支持
  • 原生支持多行字符串

字符串的内部表示

1
2
3
4
5
6
// $GOROOT/src/reflect/value.go
// StringHeader是一个string的运行时表示
type stringStruct struct {
str unsafe.Pointer
len int
}

string 类型其实是一个“描述符”,它本身并不真正存储字符串数据,而仅是由一个指向底层存储的指针和字符串的长度字段组成的。

string 类型变量在 Go 内存中的存储:

Go 编译器把源码中的 string 类型映射为运行时的一个二元组(Data, Len),真实的字符串值数据就存储在一个被 Data 指向的底层数组中。通过 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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
var sl []string = []string{
"Rob Pike ",
"Robert Griesemer ",
"Ken Thompson ",
}

func concatStringByOperator(sl []string) string {
var s string
for _, v := range sl {
s += v
}
return s
}

func concatStringBySprintf(sl []string) string {
var s string
for _, v := range sl {
s = fmt.Sprintf("%s%s", s, v)
}
return s
}

func concatStringByJoin(sl []string) string {
return strings.Join(sl, "")
}

func concatStringByStringsBuilder(sl []string) string {
var b strings.Builder
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}

func concatStringByStringsBuilderWithInitSize(sl []string) string {
var b strings.Builder
b.Grow(64)
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}

func concatStringByBytesBuffer(sl []string) string {
var b bytes.Buffer
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}

func concatStringByBytesBufferWithInitSize(sl []string) string {
buf := make([]byte, 0, 64)
b := bytes.NewBuffer(buf)
for _, v := range sl {
b.WriteString(v)
}
return b.String()
}

func BenchmarkConcatStringByOperator(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByOperator(sl)
}
}

func BenchmarkConcatStringBySprintf(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringBySprintf(sl)
}
}

func BenchmarkConcatStringByJoin(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByJoin(sl)
}
}

func BenchmarkConcatStringByStringsBuilder(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByStringsBuilder(sl)
}
}

func BenchmarkConcatStringByStringsBuilderWithInitSize(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByStringsBuilderWithInitSize(sl)
}
}

func BenchmarkConcatStringByBytesBuffer(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByBytesBuffer(sl)
}
}

func BenchmarkConcatStringByBytesBufferWithInitSize(b *testing.B) {
for n := 0; n < b.N; n++ {
concatStringByBytesBufferWithInitSize(sl)
}
}

/*
$ go test -bench . -benchmem string_concat_benchmark_test.go
goos: linux
goarch: amd64
cpu: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz
BenchmarkConcatStringByOperator-8 16233814 74.30 ns/op 80 B/op 2 allocs/op
BenchmarkConcatStringBySprintf-8 3572353 339.8 ns/op 176 B/op 8 allocs/op
BenchmarkConcatStringByJoin-8 28999772 45.75 ns/op 48 B/op 1 allocs/op
BenchmarkConcatStringByStringsBuilder-8 14822047 85.82 ns/op 112 B/op 3 allocs/op
BenchmarkConcatStringByStringsBuilderWithInitSize-8 31907448 36.64 ns/op 64 B/op 1 allocs/op
BenchmarkConcatStringByBytesBuffer-8 19904794 66.63 ns/op 112 B/op 2 allocs/op
BenchmarkConcatStringByBytesBufferWithInitSize-8 30484027 39.38 ns/op 48 B/op 1 allocs/op
PASS
ok command-line-arguments 9.405s
*/

从基准测试的输入结果的第三列,即每次操作耗时的数值来看:

  • 做了预初始化的strings.Builder连接构造字符串的效率最高;
  • 带有预初始化的bytes.Bufferstrings.Join这两种方法效率接近;
  • 未做预初始化的strings.Builderbytes.Buffer和操作符连接在第三档次;
  • fmt.Sprintf性能最差。

结论:

  • 在能预估出最终字符串长度的情况下,使用预初始化的strings.Builder连接构建字符串效率最高;
  • strings.Join连接构建字符串的平均性能最稳定;
  • 使用操作符连接的方式最直观,并且可以得到编译器的优化处理;
  • fmt.Sprintf效率不高,但适合由多种不同类型的变量来构建特定格式的字符串。

字符串相关的高效转换

[]rune或[]byte 反向转换为 string:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func main() {
rs := []rune{
0x4E2D,
0x56FD,
0x6B22,
0x8FCE,
0x60A8,
}

s := string(rs)
fmt.Println(s) // 中国欢迎您

sl := []byte{
0xE4, 0xB8, 0xAD,
0xE5, 0x9B, 0xBD,
0xE6, 0xAC, 0xA2,
0xE8, 0xBF, 0x8E,
0xE6, 0x82, 0xA8,
}

s = string(sl)
fmt.Println(s) // 中国欢迎您
}

转换是要付出代价的,根源在于string是不可变的,运行时要为转换后的类型分配新内存。

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
func byteSliceToString() {
sl := []byte{
0xE4, 0xB8, 0xAD,
0xE5, 0x9B, 0xBD,
0xE6, 0xAC, 0xA2,
0xE8, 0xBF, 0x8E,
0xE6, 0x82, 0xA8,
0xEF, 0xBC, 0x8C,
0xE5, 0x8C, 0x97,
0xE4, 0xBA, 0xAC,
0xE6, 0xAC, 0xA2,
0xE8, 0xBF, 0x8E,
0xE6, 0x82, 0xA8,
}

_ = string(sl)
}

func stringToByteSlice() {
s := "中国欢迎您,北京换欢您"
_ = []byte(s)
}

func main() {
fmt.Println(testing.AllocsPerRun(1, byteSliceToString)) // 1
fmt.Println(testing.AllocsPerRun(1, stringToByteSlice)) // 1
}

针对“中国欢迎您,北京欢迎您”这个长度的字符串,在string与byte slice互转的过程中都要有一次内存分配操作。

因此,想要更高效地进行转换,唯一的办法就是减少甚至避免额外的内存分配操作

------ 本文结束------
如果本篇文章对你有帮助,可以给作者加个鸡腿~(*^__^*),感谢鼓励与支持!