Go內存中的字符串操作
內存中的字符串類型詳細描述了字符串在內存中的結構及其類型信息。
本文主要研究字符串的各種操作(語法糖),在內存中實際的樣子。
環境
- OS : Ubuntu 20.04.2 LTS; x86_64
- Go : go version go1.16.2 linux/amd64
聲明
操作系統、處理器架構、Go版本不同,均有可能造成相同的源碼編譯后運行時的寄存器值、內存地址、數據結構不同。
本文僅保證學習過程中的分析數據在當前環境下的準確有效性。
操作類型
比較
- 相等性比較
- 不等性比較
連接(相加)
與[]byte的轉換
與[]byte的拷貝
代碼清單
- package main
- import (
- "fmt"
- )
- func main() {
- var array [20]byte
- var s = "copy hello world"
- string2slice(s)
- copyString(array[:], s)
- slice2string(array[:])
- compare()
- concat()
- }
- //go:noinline
- func copyString(slice []byte, s string) {
- copy(slice, s)
- PrintSlice(slice)
- }
- //go:noinline
- func string2slice(s string) {
- PrintSlice([]byte(s))
- }
- //go:noinline
- func slice2string(slice []byte) {
- PrintString(string(slice))
- }
- //go:noinline
- func compare() {
- var h = "hello"
- var w = "world!"
- PrintBool(h > w)
- PrintBool(h < w)
- PrintBool(h >= w)
- PrintBool(h <= w)
- PrintBool(h != w) // PrintBool(true)
- PrintBool(h == w) // PrintBool(false)
- PrintBool(testEqual(h, w))
- PrintBool(testNotEqual(h, w))
- }
- //go:noinline
- func testEqual(h, w string) bool {
- return h == w
- }
- //go:noinline
- func testNotEqual(h, w string) bool {
- return h != w
- }
- //go:noinline
- func concat() {
- hello := "hello "
- world := "world"
- jack := "Jack"
- rose := " Rose "
- lucy := "Lucy"
- lily := " Lily "
- ex := "!"
- PrintString(concat2(hello, world))
- PrintString(concat3(hello, jack, ex))
- PrintString(concat4(hello, jack, rose, ex))
- PrintString(concat5(hello, jack, rose, lucy, lily))
- PrintString(concat6(hello, jack, rose, lucy, lily, ex))
- }
- //go:noinline
- func concat2(a, b string) string {
- return a + b
- }
- //go:noinline
- func concat3(a, b, c string) string {
- return a + b + c
- }
- //go:noinline
- func concat4(a, b, c, d string) string {
- return a + b + c + d
- }
- //go:noinline
- func concat5(a, b, c, d, e string) string {
- return a + b + c + d + e
- }
- //go:noinline
- func concat6(a, b, c, d, e, f string) string {
- return a + b + c + d + e + f
- }
- //go:noinline
- func PrintBool(v bool) {
- fmt.Println("v =", v)
- }
- //go:noinline
- func PrintString(v string) {
- fmt.Println("s =", v)
- }
- //go:noinline
- func PrintSlice(s []byte) {
- fmt.Println("slice =", s)
- }
- 添加go:noinline注解避免內聯,方便指令分析
- 定義PrintBool/PrintSlice/PrintString函數避免編譯器插入runtime.convT*函數調用
深入內存
字符串轉[]byte
代碼清單中的string2slice函數代碼非常簡單,用于觀察[]byte(s)具體實現邏輯,編譯之后指令如下:

可以清晰地看到,我們在代碼中的[]byte(s),被Go編譯器替換為runtime.stringtoslicebyte函數調用。
runtime.stringtoslicebyte函數定義在runtime/string.go源碼文件中,Go編譯器傳遞給該函數的buf參數值為nil。
- func stringtoslicebyte(buf *tmpBuf, s string) []byte {
- var b []byte
- if buf != nil && len(s) <= len(buf) {
- *buf = tmpBuf{}
- b = buf[:len(s)]
- } else {
- b = rawbyteslice(len(s))
- }
- copy(b, s)
- return b
- }
rawbyteslice函數的功能是申請一塊內存用于存儲拷貝后的數據。
[]byte轉字符串
代碼清單中的slice2string函數代碼非常簡單,用于觀察string(slice)具體實現邏輯,編譯之后指令如下:
可以清晰地看到,我們在代碼中的string(slice),被Go編譯器替換為runtime.slicebytetostring函數調用。
runtime.slicebytetostring函數定義在runtime/string.go源碼文件中,Go編譯器傳遞給該函數的buf參數值為nil。
拷貝字符串到[]byte
代碼清單中的copyString函數代碼非常簡單,用于觀察copy(slice, s)具體實現邏輯,編譯之后指令如下:
這個邏輯稍微復雜一點點,將以上指令再次翻譯為Go偽代碼如下:
- func copyString(slice reflect.SliceHeader, s reflect.StringHeader) {
- n := slice.Len
- if slice.Len > s.Len {
- n = s.Len
- }
- if slice.Data != s.Data {
- runtime.memmove(slice.Data, s.Data, n)
- }
- PrintSlice(*(*[]byte)(unsafe.Pointer(&slice)))
- }
可以看到,Go編譯器在copy(slice, s)這個簡單易用語法糖背后做了很多的工作。
經過比較,以上偽代碼與runtime/slice.go源碼文件中的slicecopy函數非常相似,但又不完全一致。
不等性比較
代碼清單中的compare函數測試了兩個字符串的各種比較操作。
查看該函數的指令,發現Go編譯器將以下四種比較操作全部轉換為runtime.cmpstring函數調用:
- >
- <
- >=
- <=
runtime.cmpstring函數是一個編譯器函數,不會被直接調用,聲明在cmd/compile/internal/gc/builtin/runtime.go源碼文件中,由匯編語言實現。
GOARCH=amd64的實現位于internal/bytealg/compare_amd64.s源碼文件中。
該函數返回值可能是:
然后使用cmp匯編指令將返回值與0進行比較,再使用以下匯編指令保存最終的比較結果(true / false):
在本例中,有兩個特殊的比較,分別被編譯為單條指令:
- h != w 被編譯為 movb $0x1,(%rsp)
- h == w 被編譯為 movb $0x0,(%rsp)
這是因為在本例中編譯器知道"hello"與"world"兩個字符串不相等,所以直接在編譯的時候直接把比較結果編譯到機器指令中。
所以,在代碼定義了testEqual和testNotEqual函數用于比較字符串變量。
相等性比較
關于相等性比較,在 內存中的字符串類型 中已經做了非常詳細的分析和說明。
在本文的代碼清單中,testEqual函數指令如下,與runtime.strequal函數一致,是因為編譯器將runtime.strequal函數內聯(inline)到了testEqual函數中。
出乎意料的是,!=與==編譯后的幾乎一致,只是兩處指令對結果進行了相反的操作:
字符串連接(相加)
在本文的代碼清單中,concat函數用于觀察字符串的連接(+)操作,測試結果表明:
- 2個字符串相加,實際調用runtime.concatstring2函數
- 3個字符串相加,實際調用runtime.concatstring3函數
- 4個字符串相加,實際調用runtime.concatstring4函數
- 5個字符串相加,實際調用runtime.concatstring5函數
- 超過5個字符串相加,實際調用runtime.concatstrings函數
以上這些函數調用,都是Go編譯器的代碼生成和插入工作。
在插入runtime.concatstring*函數的過程中,編譯器傳遞給這些函數的buf參數的值為nil。
runtime.concatstring*函數的實現非常簡單,這里不再進一步贅述。
小結
從以上詳細的分析可以看到,我們在開發過程中,所有對字符串進行的簡單操作,都會被Go編譯器編碼為復雜的指令和函數調用。
許多開發者喜歡使用Go進行開發,理由是Go語言非常簡單、簡潔。
是的,我們都喜歡這種甜甜的語法糖。
而且,發掘語法糖背后的秘密,也是很好玩的事。
本文轉載自微信公眾號「Golang In Memory」