Go:如何高效地拼接字符串
幾個月後,該項目在原基礎上需求有所增加。再次面對該代碼,心情也發生了變化,使用 strings.Builder
固然可行,但它需要三行代碼,相比之下,用 +
操作符一行代碼便能實現。在簡潔性和高效性之間,該如何抉擇呢?Go 語言中,是否有魚和熊掌兼得的方法?
抱着這樣的心思,我總結出 Go 語言中至少有 6 種關於字符串的拼接方式。但新問題也隨之產生了,爲何 Go 語言支持如此多種拼接方式?每種方式的存在,其背後的原因和邏輯又是什麼呢?
讓我們先分兩種常用場景、兩種字符串長度進行對比看看。
一、不同情景下的效率測試
待拼接字符串長度、次數已知,可一次完成字符串拼接,測試結果如下:
-
32 字節以下。
-
超過 32 字節,未超過 64 字節。
+
操作符發生了一次內存分配,但效率依然很高。bytes.Buffer
高於strings.Builder
。 -
64 字節以上。
+
操作符優勢依然明顯,strings.Join()
也不甘示弱。
待拼接字符串長度、次數未知,需要循環追加完成拼接操作,測試結果如下:
-
32 字節以下。
+
每次拼接都會生成新字符串,導致大量的字符串創建、替代。
-
超過 32 字節,未超過 64 字節。
bytes.Buffer
發生 2 次內存分配。
-
64 字節以上。
bytes.Buffer
優勢已然不再,strings.Builder
一騎絕塵。不過,這似乎還不是最終的結果。 -
大量字符串拼接,終於要使出
strings.Builder
的必殺器Grow()
了,bytes.Buffer
也有Grow()
方法,但似乎作用不大。
二、原理分析
從上面的測試結果可以看出,在不同情況下,每種拼接方式的效率都不同,爲什麼會這樣呢?那就得從它們的拼接原理說起。
-
+=
追加操作符,與+
操作符相同,也是通過runtime/string.go的concatstrings()
函數實現拼接,區別是它通常用於循環中往字符串末尾追加,每追加一次,生成一個新的字符串替代舊的,效率極低。res := "發" res += "發"
拼接過程:
- 同上
-
bytes.Buffer
,在 Golang 1.10 之前,它是循環中往末尾追加效率最高的方法,尤其是當拼接的字符串數量較大時。var b bytes.Buffer // 創建一個 buffer b.WriteString("發") // 將字符串追加到buffer上 b.WriteString("發") b.String() // 取出字符串並返回
拼接過程:
- 創建 []byte ,用於緩存需要拼接的字符串
- 首次使用 WriteString() 填充字符串時,由於字節數組容量爲 0 ,最少會發生 1 次內存分配
- 待拼接字符串長度小於 64 字節,make 一個長度爲字符串總長度,容量爲 64 字節的新數組
- 待拼接字符串超過 64 字節時動態擴容,按 2* 當前容量 + 待拼接字符長度 make 新字節數組
- 將字節數組轉換成 string 類型返回
-
strings.Builder
在 Golang 1.10 更新後,替代了byte.Buffer
,成爲號稱效率最高的拼接方法。var b strings.Builder b.WriteString("發") b.WriteString("發") b.String()
拼接過程:
- 創建 []byte,用於緩存需要拼接的字符串
- 通過 append 將數據填充到前面創建的 []byte 中
3.append 時,如果字符串超過初始容量 8 且小於 1024 字節時,按乘以 2 的容量創建新的字節數組,超過 1024 字節時,按 1/4 增加 - 將老數據複製到新創建的字節數組中 5. 追加新數據並返回
-
strings.Join()
主要適用於以指定分隔符方式連接成一個新字符串,分隔符可以爲空,在字符串一次拼接操作中,性能僅次於+
操作符。strings.Join([]string{"發", "發"}, "")
拼接過程:
- 接收的是一個字符切片
- 遍歷字符切片得到總長度,據此通過 builder.Grow 分配內存
- 底層使用了
strings.Builder
,每使用一次strings.Join()
,都會創建新的 builder 對象
-
fmt.Sprintf()
,返回使用 format 格式化的參數。除了字符串拼接,函數內還有很多格式方面的判斷,性能不高,但它可以拼接多種類型,字符串或數字等。fmt.Sprintf("str1 = %v,str2 = %v", "發", "發")
拼接過程:
- 創建對象
- 字符串格式化操作
- 將格式化後的字符串通過 append 方式放到 [] byte 中
- 最後將字節數組轉換成 string 返回
三、結論
在待拼接字符串確定,可一次完成字符串拼接的情況下,推薦使用 +
操作符,即便 strings.Builder
用 Grow()
方法預先擴容,其性能也是不如 +
操作符的,另外,Grow()
也不可設置過大。
在拼接字符串不確定、需要循環追加字符串時,推薦使用 strings.Builder
。但在使用時,必須使用 Grow()
預先擴容,否則性能不如 strings.Join()
。
我爲大家整理了一份從入門到進階的 Go 學習資料禮包,包含學習建議:入門看什麼,進階看什麼。關注公衆號 「polarisxu」,回覆 ebook 獲取;還可以回覆「進羣」,和數萬 Gopher 交流學習。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/9328Ju9pF80djNtRXqfSXQ