日前在看 Go runtime/string.go source code 的時候,意外看到一篇文章 Go中string转[]byte的陷阱,不過這篇文章的日期間隔已久,Go version 也從文章中的 1.10 改版到 1.14 了,所以想要更新一下這個議題現況。
Version
Go version: 1.14
問題摘要
1. runtime.stringtoslicebyte
首先,文章中提到的例子:
1func main() {
2 s := []byte("")
3 s1 := append(s, 'a')
4 s2 := append(s, 'b')
5 //fmt.Println(s1, "==========", s2)
6 fmt.Println(string(s1), "==========", string(s2))
7}
在沒有 fmt.Println(s1, "==========", s2)
這一行的情況下,output 會產生:
1b ========== b
的結果。
反之,如果加了 fmt.Println(s1, "==========", s2)
,由於 Escape Analysis 的關係,因此結果會變成:
1a ========== b
此陷阱是由於在一開始宣告 byte slice 時,是由 string 轉換成 slice
1s := []byte("")
這樣的寫法會觸發 runtime.stringtoslicebyte
,細究此 function:
1func stringtoslicebyte(buf *tmpBuf, s string) []byte {
2 var b []byte
3 if buf != nil && len(s) <= len(buf) {
4 *buf = tmpBuf{}
5 b = buf[:len(s)]
6 } else {
7 b = rawbyteslice(len(s))
8 }
9 copy(b, s)
10 return b
11}
會發現 escape analysis 結果 (buf != nil
) 有可能影響最後回傳的 []byte slice,進而導致後續 slice append 行為不同。
2. byte slice initialization
另外,在 issue cmd/compile: constant string -> []byte and []byte -> string conversions aren’t constant folded 中也提到 byte 的行為不一致:
1a := []byte("foo")
2b := []byte{'f','o','o'}
因為前者 a 會觸發 runtime.stringtoslicebyte
的關係,所以 []byte("foo")
比起 []byte{'f','o','o'}
需要額外的執行成本,但是對使用者來說, []byte("foo")
應該是更常見的寫法。
Optimization
因此,在 cmd/compile: make []byte("…") more efficient 這項 commit 就針對
[]byte(string) conversions
行為做改進,不再使用 stringtoslicebyte
,而是定義一個與 string 相同長度的 byte array (記憶體空間根據 escape analysis 可能在 heap 或是 goroutine stack),並且使用 string 來將此 array 初始化,再轉換成 slice 回傳。
實作方式可參閱下面的 asm code,可以看到已和陷阱文章中所提到的 asm code 有明顯差異。
1// s := []byte("abc")
2
30x0021 00033 PCDATA $0, $1
40x0021 00033 PCDATA $1, $0
50x0021 00033 LEAQ type.[3]uint8(SB), AX
60x0028 00040 PCDATA $0, $0
70x0028 00040 MOVQ AX, (SP)
80x002c 00044 CALL runtime.newobject(SB)
90x0031 00049 PCDATA $0, $1
100x0031 00049 MOVQ 8(SP), AX
110x0036 00054 MOVW $25185, (AX)
120x003b 00059 MOVB $99, 2(AX)
這樣的更動自然地也就消除了一開始所提到的 string 轉 byte slice 陷阱,自 Go 1.12 版本之後,[]byte(string)
會回傳 cap, len 值皆為 string 長度的 byte slice,而不會受到 runtime.stringtoslicebyte
邏輯因素而產生 cap 為 32 的 slice (具體內容可詳見Go中string转[]byte的陷阱)。
總結
由於改善 []byte(string)
的 compile 結果,也消除了之前可能會遇到的陷阱,這其中從發現問題到修改過程都有值得讓人好好學習的知識和概念。