摘要:由於在實習過程中,做的項目都是基於 Golang 語言,所以在面試時,面試官也一定會理所當然的問 Golang, 所以在最近一段時間,主要學習這門語言的基礎知識,以及常出的面試題。
簡單介紹
字符串雖然在 Go 語言中是基本類型 string, 但是它實際上是由字符組成的數組,類似於 C 語言中的 char [] ,作為數組會占用一片連續的內存空間。Go 語言中的字符串其實只是一個只讀的字節數組,不支持直接修改 string 類型變量的內存空間,比如下面代碼就是不支持的:

package main import ( "fmt" ) func main() { s := "hello" s[0] = 'A' fmt.Println(s) } //.\main.go:9:7: cannot assign to s[0]
如果我們想修改字符串,我們可以將這段內存拷貝到堆或者棧上,將遍歷的類型轉換為 []byte 之后就可以進行,修改后通過類型轉換就可以變回 string, 對原變量重新賦值即可。

package main import ( "fmt" ) func main() { s := "hello" sByte := []byte(s) sByte[0] = 'A' //重新賦值 s = string(sByte) fmt.Println(s) } //Aello
數據結構
字符串在 Go 語言中的接口其實非常簡單,每一個字符串在運行時都會使用如下的 StringHeader 結構體表示,其實在”運行時“內部,有一個私有的結構 stringHeader, 它有着完全相同的結構,只是用於存儲數據的 Data 字段使用了 unsafe.Pointer 類型:

// StringHeader is the runtime representation of a string. // It cannot be used safely or portably and its representation may // change in a later release. // Moreover, the Data field is not sufficient to guarantee the data // it references will not be garbage collected, so programs must keep // a separate, correctly typed pointer to the underlying data. type StringHeader struct { Data uintptr Len int } // stringHeader is a safe version of StringHeader used within this package. type stringHeader struct { Data unsafe.Pointer Len int }
聲明方式
使用雙引號
s := "hello world"
使用反引號
s := `hello world`
使用雙引號可其它語言沒有什么大的區別,如果字符串內部出現雙引號,要使用 \ 進行轉義;但使用反引號則不需要,方便進行更加復雜的數據類型,比如 Json:
s := `{"name": "sween", "age": 18}`
注:上面兩種格式的解析函數分別為cmd/compile/internal/syntax.scanner.stdString
cmd/compile/internal/syntax.scanner.rawString
類型轉換
在我們使用 Go 語言解析和序列化 Json 等數據格式時,經常需要將數據在 string 和 []byte 之間進行轉換,類型轉換的開銷其實並沒有想象中的那么小。
[]byte 到 string 的轉換
runtime.slicebytetostring 這個函數中進行轉換的處理,我們看下源碼:

// slicebytetostring converts a byte slice to a string. // It is inserted by the compiler into generated code. // ptr is a pointer to the first element of the slice; // n is the length of the slice. // Buf is a fixed-size buffer for the result, // it is not nil if the result does not escape. func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) { if n == 0 { // Turns out to be a relatively common case. // Consider that you want to parse out data between parens in "foo()bar", // you find the indices and convert the subslice to string. return "" } if n == 1 { p := unsafe.Pointer(&staticuint64s[*ptr]) if sys.BigEndian { p = add(p, 7) } stringStructOf(&str).str = p stringStructOf(&str).len = 1 return } var p unsafe.Pointer if buf != nil && n <= len(buf) { p = unsafe.Pointer(buf) } else { //step1: 分配內存空間 p = mallocgc(uintptr(n), nil, false) } stringStructOf(&str).str = p stringStructOf(&str).len = n //step2:執行內存拷貝操作 memmove(p, unsafe.Pointer(ptr), uintptr(n)) return }
string 到 []byte 的轉換
runtime.stringtoslicebyte 這個函數中進行轉換的處理,我們看下源碼:

func stringtoslicebyte(buf *tmpBuf, s string) []byte { var b []byte if buf != nil && len(s) <= len(buf) { //step1: 如果緩沖區夠用,直接用 *buf = tmpBuf{} b = buf[:len(s)] } else { //step2: 如果緩沖區不夠用,重新分配一個 b = rawbyteslice(len(s)) } //step3: 執行內存拷貝操作 copy(b, s) return b } // rawbyteslice allocates a new byte slice. The byte slice is not zeroed. func rawbyteslice(size int) (b []byte) { cap := roundupsize(uintptr(size)) p := mallocgc(cap, nil, false) if cap != uintptr(size) { memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size)) } *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)} return }
總結
字符串和 []byte 中的內容雖然一樣,但是字符串的內容是只讀的,我們不能通過下標或者其它形式改變其中的數據,而 []byte 中的內容是可讀寫的,無論哪種類型轉換到另一種類型都需要對其中的內容進行拷貝,而內存拷貝的性能損耗會隨着字符串和 []byte 長度的增長而增長。所以在做類型轉換時候一定要注意性能的損耗。
參考資料:
https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-string/