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 可以爲空(長度爲 0),但不會是 nil;
-
string 對象不可以修改。
string 數據結構
源碼包src/runtime/string.go:stringStruct
定義了 string 的數據結構:
1type stringStruct struct {
2 str unsafe.Pointer
3 len int
4}
5
6
其數據結構很簡單:
-
stringStruct.str:字符串的首地址;
-
stringStruct.len:字符串的長度;
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
需要注意的是這種轉換需要一次內存拷貝。
轉換過程如下:
-
跟據切片的長度申請內存空間,假設內存地址爲 p,切片長度爲 len(b);
-
構建 string(string.str = p;string.len = len;)
-
拷貝數據 (切片中數據拷貝到新申請的內存空間)
轉換示意圖:
string 轉 []byte
string 也可以方便的轉成 byte 切片,如下所示:
1func GetSliceByString(str string) []byte {
2 return []byte(str)
3}
4
5
string 轉換成 byte 切片,也需要一次內存拷貝,其過程如下:
-
申請切片內存空間
-
將 string 拷貝到切片
轉換示意圖:
字符串拼接
字符串可以很方便的拼接,像下面這樣:
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) 指向切片的內存。
比如,編譯器會識別如下臨時場景:
-
使用 m[string(b)] 來查找 map(map 是 string 爲 key,臨時把切片 b 轉成 string);
-
字符串拼接,如 "<" + "string(b)" + ">";
-
字符串比較:string(b) == "foo"
因爲是臨時把 byte 切片轉換成 string,也就避免了因 byte 切片同容改成而導致 string 引用失敗的情況,所以此時可以不必拷貝內存新建一個 string。
string 和 []byte 如何取捨
string 和 []byte 都可以表示字符串,但因數據結構不同,其衍生出來的方法也不同,要跟據實際應用場景來選擇。
string 擅長的場景:
-
需要字符串比較的場景;
-
不需要 nil 字符串的場景;
[]byte 擅長的場景:
-
修改字符串的場景,尤其是修改粒度爲 1 個字節;
-
函數返回值,需要用 nil 表示含義的場景;
-
需要切片操作的場景;
雖然看起來 string 適用的場景不如 []byte 多,但因爲 string 直觀,在實際應用中還是大量存在,在偏底層的實現中 []byte 使用更多。
轉自:戀戀美食
鏈接:https://my.oschina.net/renhc/blog/3019849
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/6D9RLaQrtjBwso__ae2zAQ