Go常用数据结构

高效拼接字符串

拼接字符串的方式

  • fmt.Sprintf
  • bytes.buffer
  • strings.builder
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

const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func randomString(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Intn(len(letterBytes))]
}
return string(b)
}

func plusConcat(n int, str string) string {
s := ""
for i := 0; i < n; i++ {
s += str
}
return s
}

func sprintfConcat(n int, str string) string {
s := ""
for i := 0; i < n; i++ {
s = fmt.Sprintf("%s%s", s, str)
}
return s
}

func builderConcat(n int, str string) string {
var builder strings.Builder
for i := 0; i < n; i++ {
builder.WriteString(str)
}
return builder.String()
}

func bufferConcat(n int, s string) string {
buf := new(bytes.Buffer)
for i := 0; i < n; i++ {
buf.WriteString(s)
}
return buf.String()
}

func byteConcat(n int, str string) string {
buf := make([]byte, 0)
for i := 0; i < n; i++ {
buf = append(buf, str...)
}
return string(buf)
}

func preByteConcat(n int, str string) string {
buf := make([]byte, 0, n*len(str))
for i := 0; i < n; i++ {
buf = append(buf, str...)
}
return string(buf)
}

benchmark 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

func benchmark(b *testing.B, f func(int, string) string) {
var str = randomString(10)
for i := 0; i < b.N; i++ {
f(10000, str)
}
}

func BenchmarkPlusConcat(b *testing.B) { benchmark(b, plusConcat) }
func BenchmarkSprintfConcat(b *testing.B) { benchmark(b, sprintfConcat) }
func BenchmarkBuilderConcat(b *testing.B) { benchmark(b, builderConcat) }
func BenchmarkBufferConcat(b *testing.B) { benchmark(b, bufferConcat) }
func BenchmarkByteConcat(b *testing.B) { benchmark(b, byteConcat) }
func BenchmarkPreByteConcat(b *testing.B) { benchmark(b, preByteConcat) }

测试结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Forrest@LAPTOP-32G4HDVI MINGW64 /d/Go_WorkSpace/面经/connectStrings (main)
$ go test -bench="Concat$" -benchmem .
goos: windows
goarch: amd64
pkg: demo
cpu: AMD Ryzen 7 5800H with Radeon Graphics
BenchmarkPlusConcat-16 16 79042431 ns/op 530998053 B/op 10024 allocs/op
BenchmarkSprintfConcat-16 9 115482900 ns/op 833870728 B/op 34211 allocs/op
BenchmarkBuilderConcat-16 9648 109838 ns/op 514801 B/op 23 allocs/op
BenchmarkBufferConcat-16 12285 95907 ns/op 368576 B/op 13 allocs/op
BenchmarkByteConcat-16 10000 122219 ns/op 621297 B/op 24 allocs/op
BenchmarkPreByteConcat-16 21231 59183 ns/op 212993 B/op 2 allocs/op
PASS
ok demo 8.840s

其中的 -bench=Concat$ 中的$表示行尾,因此这将匹配以”Concat”结尾的测试函数。

拼接大量字符串优先使用 stirngs.builder

strings.builder 的扩容策略

1
2
3
4
5
6
7
8
9
10
11
12
func TestBuilderCap(t *testing.T) {
str := randomString(10)
var builder strings.Builder
cap := 0
for i := 0; i < 100000; i++ {
builder.WriteString(str)
if cap != builder.Cap() {
cap = builder.Cap()
fmt.Printf("%d ", cap)
}
}
}

运行结果

1
2
3
4
5
6
Forrest@LAPTOP-32G4HDVI MINGW64 /d/Go_WorkSpace/面经/connectStrings (main)
$ go test -v
=== RUN TestBuilderConcat
16 32 64 128 256 512 896 1408 2048 3072 4096 5376 6912 9472 12288 16384 21760 28672 40960 57344 73728 98304 131072 --- PASS: TestBuilderConcat (0.00s)
PASS
ok demo 0.709s

go 中的slice是 可变长的数据序列。

go 是值传递,其中func中的 参数也是值传递,只是传递的是 pointer,同一个pointer,所以更改后会对原slice的值造成影响

其中由 指向array的指针+len+cap 三个字段构成slice。

扩容策略:

  • 若容量超过系统限制,直接panic
  • 若容量为0,返回 zerobase slice{unsafe.Pointer($zerobase),old.len,size}
  • 当cap小于 256时,扩容机制为:直接翻倍;
    • 当cap大于256时,扩容策 略为:在原容量 n 的基础上循环执行 n += (n+3*256)/4,直到 n 大于等于预期新容量,并取 n 作为新容量。即:扩容1/4 +192 的固定值
  • 计算相应的数据结构所需的内存大小,分配内存
  • 数据迁移
  • 返回新slice
1
2
3
4
5
func Test_slice(t *testing.T){
s := make([]int,512)
s = append(s,1)
t.Logf("len of s: %d, cap of s: %d",len(s),cap(s))
}
1
len: 513, cap: 848

Go常用数据结构
http://example.com/2024/01/12/Go常用数据结构/
作者
Forrest
发布于
2024年1月12日
许可协议