在 Go 語言中,如何高效的處理字串相加,由於字串 (string) 是不可變的,所以將很多字串拼接起來,會如同宣告新的變數來儲存。這邊就可以透過 strings.Builder 或 bytes.Buffer 來解決字串相加效能問題。除了效能問題之外,還需要注意在 bytes.Buffer
處理 []byte
及 string
之間的轉換,底下拿實際專案上寫出來的錯誤給大家參考看看
bytes.Buffer 重複使用問題
專案用 bytes.Buffer 套件處理資料 Parsing 後的結果,底下是一個基本範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| package main
import (
"bytes"
"fmt"
)
var buf bytes.Buffer
func parseMultipleValue(n int, str string) []byte {
buf.Reset()
for i := 0; i < n; i++ {
buf.WriteString(str)
}
return buf.Bytes()
}
func main() {
s1 := parseMultipleValue(5, "1")
fmt.Println("s1:", string(s1))
s2 := parseMultipleValue(3, "2")
fmt.Println("s1:", string(s1))
fmt.Println("s2:", string(s2))
}
|
請直接線上打開範例跑看看,執行後的結果會是
1
2
3
| s1: 11111
s1: 22211
s2: 222
|
大家有無看到,如果要存取 s1
第二次的結果,會發現後者 s2
資料蓋掉部分 s1
資料。原因是這樣,當第一次 s1 拿到的是有 5 位元空間的記憶體,而當執行第二次 parseMultipleValue
後,透過 bytes.Rest()
只是將 offset 位置移動到 0 位置,並將新的內容給寫入到同樣記憶體位置前面區段。固本來 s1
的內容前 3 個字元被改成新的 s2
字串。
兩種解決方式
該怎麼做可以不影響 s1
的內容呢?直接用 bytes.Buffer 內建函示 String()
可以解決此問題。
1
2
3
4
5
6
7
8
9
| var buf bytes.Buffer
func parseMultipleValue(n int, str string) string {
buf.Reset()
for i := 0; i < n; i++ {
buf.WriteString(str)
}
return buf.String()
}
|
如果不透過 String()
解決的話,也可以透過 copy 方式來處理,並且使用 unsafe.Pointer
來做 byte 轉 string 的效能優化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| var buf bytes.Buffer
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
func parseMultipleValue(n int, str string) string {
buf.Reset()
for i := 0; i < n; i++ {
buf.WriteString(str)
}
s := make([]byte, len(buf.Bytes()))
copy(s, buf.Bytes())
return b2s(s)
}
|
上述兩種解法最終都能解決問題,效能也沒有差異,故大家可以選其中一種即可。
1
2
3
4
| BenchmarkA
BenchmarkA-8 34922 33986 ns/op 106496 B/op 1 allocs/op
BenchmarkB
BenchmarkB-8 35760 33714 ns/op 106496 B/op 1 allocs/op
|
附上完整程式碼
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
| package main
import (
"bytes"
"math/rand"
"testing"
"unsafe"
)
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)
}
var buf bytes.Buffer
func b2s(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
func parseMultipleValue(n int, str string) string {
buf.Reset()
for i := 0; i < n; i++ {
buf.WriteString(str)
}
s := make([]byte, len(buf.Bytes()))
copy(s, buf.Bytes())
return b2s(s)
}
func parseMultipleValue2(n int, str string) string {
buf.Reset()
for i := 0; i < n; i++ {
buf.WriteString(str)
}
return buf.String()
}
func benchmark(b *testing.B, f func(int, string) string) {
str := randomString(10)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
f(10000, str)
}
}
func BenchmarkA(b *testing.B) { benchmark(b, parseMultipleValue) }
func BenchmarkB(b *testing.B) { benchmark(b, parseMultipleValue2) }
|
心得
由於需要分析非常大的檔案 (200MB) 內容及時程非常趕,故沒有寫完整的測試,才沒發現這個錯誤,果然自己的一時疏忽,造成這個失誤,補上完整的測試後,就可以再陸續針對效能進行優化。
See also