Go string 實現原理剖析

【導讀】作爲 golang 程序員,你真的瞭解 go 中的 string 嗎?本文從 string 設計切入、介紹了 go string 和 []byte 之間的區別、與各自適用的場景。

string 標準概念

Go 標準庫builtin給出了所有內置類型的定義。源代碼位於src/builtin/builtin.go,其中關於 string 的描述如下:

1// string is the set of all strings of 8-bit bytes, conventionally but not
2// necessarily representing UTF-8-encoded text. A string may be empty, but
3// not nil. Values of string type are immutable.
4type string string
5
6

所以 string 是 8 比特字節的集合,通常但並不一定是 UTF-8 編碼的文本。

另外,還提到了兩點,非常重要:

string 數據結構

源碼包src/runtime/string.go:stringStruct定義了 string 的數據結構:

1type stringStruct struct {
2 str unsafe.Pointer
3 len int
4}
5
6

其數據結構很簡單:

string 數據結構跟切片有些類似,只不過切片還有一個表示容量的成員,事實上 string 和切片,準確的說是 byte 切片經常發生轉換。這個後面再詳細介紹。

string 操作

聲明

如下代碼所示,可以聲明一個 string 變量變賦予初值:

1    var str string
2    str = "Hello World"
3
4

字符串構建過程是先跟據字符串構建 stringStruct,再轉換成 string。轉換的源碼如下:

1func gostringnocopy(str *byte) string { // 跟據字符串地址構建string
2 ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)} // 先構造stringStruct
3 s := *(*string)(unsafe.Pointer(&ss))                             // 再將stringStruct轉換成string
4 return s
5}
6
7

string 在 runtime 包中就是 stringStruct,對外呈現叫做 string。

[]byte 轉 string

byte 切片可以很方便的轉換成 string,如下所示:

1func GetStringBySlice(s []byte) string {
2    return string(s)
3}
4
5

需要注意的是這種轉換需要一次內存拷貝。

轉換過程如下:

  1. 跟據切片的長度申請內存空間,假設內存地址爲 p,切片長度爲 len(b);

  2. 構建 string(string.str = p;string.len = len;)

  3. 拷貝數據 (切片中數據拷貝到新申請的內存空間)

轉換示意圖:

string 轉 []byte

string 也可以方便的轉成 byte 切片,如下所示:

1func GetSliceByString(str string) []byte {
2    return []byte(str)
3}
4
5

string 轉換成 byte 切片,也需要一次內存拷貝,其過程如下:

轉換示意圖:

字符串拼接

字符串可以很方便的拼接,像下面這樣:

1str := "Str1" + "Str2" + "Str3"
2
3

即便有非常多的字符串需要拼接,性能上也有比較好的保證,因爲新字符串的內存空間是一次分配完成的,所以性能消耗主要在拷貝數據上。

一個拼接語句的字符串編譯時都會被存放到一個切片中,拼接過程需要遍歷兩次切片,第一次遍歷獲取總的字符串長度,據此申請內存,第二次遍歷會把字符串逐個拷貝過去。

字符串拼接僞代碼如下:

 1func concatstrings(a []string) string { // 字符串拼接
 2    length := 0        // 拼接後總的字符串長度
 3
 4    for _, str := range a {
 5        length += length(str)
 6    }
 7
 8    s, b := rawstring(length) // 生成指定大小的字符串,返回一個string和切片,二者共享內存空間
 9
10    for _, str := range a {
11        copy(b, str)    // string無法修改,只能通過切片修改
12        b = b[len(str):]
13    }
14    
15    return s
16}
17
18

因爲 string 是無法直接修改的,所以這裏使用 rawstring() 方法初始化一個指定大小的 string,同時返回一個切片,二者共享同一塊內存空間,後面向切片中拷貝數據,也就間接修改了 string。

rawstring() 源代碼如下:

 1func rawstring(size int) (s string, b []byte) { // 生成一個新的string,返回的string和切片共享相同的空間
 2 p := mallocgc(uintptr(size), nil, false)
 3
 4 stringStructOf(&s).str = p
 5 stringStructOf(&s).len = size
 6
 7 *(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}
 8
 9 return
10}
11
12

爲什麼字符串不允許修改?

像 C++ 語言中的 string,其本身擁有內存空間,修改 string 是支持的。但 Go 的實現中,string 不包含內存空間,只有一個內存的指針,這樣做的好處是 string 變得非常輕量,可以很方便的進行傳遞而不用擔心內存拷貝。

因爲 string 通常指向字符串字面量,而字符串字面量存儲位置是隻讀段,而不是堆或棧上,所以纔有了 string 不可修改的約定。

[]byte 轉換成 string 一定會拷貝內存嗎?

byte 切片轉換成 string 的場景很多,爲了性能上的考慮,有時候只是臨時需要字符串的場景下,byte 切片轉換成 string 時並不會拷貝內存,而是直接返回一個 string,這個 string 的指針 (string.str) 指向切片的內存。

比如,編譯器會識別如下臨時場景:

因爲是臨時把 byte 切片轉換成 string,也就避免了因 byte 切片同容改成而導致 string 引用失敗的情況,所以此時可以不必拷貝內存新建一個 string。

string 和 []byte 如何取捨

string 和 []byte 都可以表示字符串,但因數據結構不同,其衍生出來的方法也不同,要跟據實際應用場景來選擇。

string 擅長的場景:

[]byte 擅長的場景:

雖然看起來 string 適用的場景不如 []byte 多,但因爲 string 直觀,在實際應用中還是大量存在,在偏底層的實現中 []byte 使用更多。

轉自:戀戀美食

鏈接:https://my.oschina.net/renhc/blog/3019849

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/6D9RLaQrtjBwso__ae2zAQ