Go string 實現原理剖析(你真的瞭解string嗎)

string標準概念

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

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

因此string是8比特字節的集合,一般但並不必定是UTF-8編碼的文本。github

另外,還提到了兩點,很是重要:數據結構

  • string能夠爲空(長度爲0),但不會是nil;
  • string對象不能夠修改。

string 數據結構

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

type stringStruct struct {
	str unsafe.Pointer
	len int
}

其數據結構很簡單:性能

  • stringStruct.str:字符串的首地址;
  • stringStruct.len:字符串的長度;

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

string操做

聲明

以下代碼所示,能夠聲明一個string變量變賦予初值:編碼

var str string
    str = "Hello World"

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

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

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

[]byte轉string

byte切片能夠很方便的轉換成string,以下所示:對象

func GetStringBySlice(s []byte) string {
    return string(s)
}

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

轉換過程以下:

  1. 跟據切片的長度申請內存空間,假設內存地址爲p,切片長度爲len(b);
  2. 構建string(string.str = p;string.len = len;)
  3. 拷貝數據(切片中數據拷貝到新申請的內存空間)

轉換示意圖:

string轉[]byte

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

func GetSliceByString(str string) []byte {
    return []byte(str)
}

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

  • 申請切片內存空間
  • 將string拷貝到切片

轉換示意圖:

字符串拼接

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

str := "Str1" + "Str2" + "Str3"

即使有很是多的字符串須要拼接,性能上也有比較好的保證,由於新字符串的內存空間是一次分配完成的,因此性能消耗主要在拷貝數據上。

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

字符串拼接僞代碼以下:

func concatstrings(a []string) string { // 字符串拼接
    length := 0        // 拼接後總的字符串長度

    for _, str := range a {
        length += length(str)
    }

    s, b := rawstring(length) // 生成指定大小的字符串,返回一個string和切片,兩者共享內存空間

    for _, str := range a {
        copy(b, str)    // string沒法修改,只能經過切片修改
        b = b[len(str):]
    }
    
    return s
}

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

rawstring()源代碼以下:

func rawstring(size int) (s string, b []byte) { // 生成一個新的string,返回的string和切片共享相同的空間
	p := mallocgc(uintptr(size), nil, false)

	stringStructOf(&s).str = p
	stringStructOf(&s).len = size

	*(*slice)(unsafe.Pointer(&b)) = slice{p, size, size}

	return
}

爲何字符串不容許修改?

像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使用更多。

贈人玫瑰手留餘香,若是以爲不錯請給個贊~

本篇文章已歸檔到GitHub項目,求星~ 點我即達

相關文章
相關標籤/搜索