日前在看 Go runtime/string.go source code 的時候,意外看到一篇文章 Go中string转[]byte的陷阱,不過這篇文章的日期間隔已久,Go version 也從文章中的 1.10 改版到 1.14 了,所以想要更新一下這個議題現況。

Version
Go version: 1.14
問題摘要
1. runtime.stringtoslicebyte
首先,文章中提到的例子:
| |
在沒有 fmt.Println(s1, "==========", s2) 這一行的情況下,output 會產生:
| |
的結果。
反之,如果加了 fmt.Println(s1, "==========", s2) ,由於 Escape Analysis 的關係,因此結果會變成:
| |
此陷阱是由於在一開始宣告 byte slice 時,是由 string 轉換成 slice
| |
這樣的寫法會觸發 runtime.stringtoslicebyte ,細究此 function:
| |
會發現 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 的行為不一致:
| |
因為前者 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 有明顯差異。
| |
這樣的更動自然地也就消除了一開始所提到的 string 轉 byte slice 陷阱,自 Go 1.12 版本之後,[]byte(string) 會回傳 cap, len 值皆為 string 長度的 byte slice,而不會受到 runtime.stringtoslicebyte 邏輯因素而產生 cap 為 32 的 slice (具體內容可詳見Go中string转[]byte的陷阱)。
總結
由於改善 []byte(string) 的 compile 結果,也消除了之前可能會遇到的陷阱,這其中從發現問題到修改過程都有值得讓人好好學習的知識和概念。