「快學 Go 語言」第 7 課——字符串


「快學 Go 語言」第 7 課——字符串


字符串通常有兩種設計,一種是「字符」串,一種是「字節」串。「字符」串中的每個字都是定長的,而「字節」串中每個字是不定長的。Go 語言裡的字符串是「字節」串,英文字符佔用 1 個字節,非英文字符佔多個字節。這意味著無法通過位置來快速定位出一個完整的字符來,而必須通過遍歷的方式來逐個獲取單個字符。


「快學 Go 語言」第 7 課——字符串


我們所說的字符通常是指 unicode 字符,你可以認為所有的英文和漢字在 unicode 字符集中都有一個唯一的整數編號,一個 unicode 通常用 4 個字節來表示,對應的 Go 語言中的字符 rune 佔 4 個字節。在 Go 語言的源碼中可以找到下面這行代碼,rune 類型是一個衍生類型,它在內存裡面使用 int32 類型的 4 個字節存儲。

<code>type rune int32
複製代碼/<code>

使用「字符」串來表示字符串勢必會浪費空間,因為所有的英文字符本來只需要 1 個字節來表示,用 rune 字符來表示的話那麼剩餘的 3 個字節都是零。但是「字符」串有一個好處,那就是可以快速定位。

為了進一步方便讀者理解字節 byte 和 字符 rune 的關係,我花了下面這張圖


「快學 Go 語言」第 7 課——字符串


其中 codepoint 是每個「字」的其實偏移量。Go 語言的字符串採用 utf8 編碼,中文漢字通常需要佔用 3 個字節,英文只需要 1 個字節。len() 函數得到的是字節的數量,通過下標來訪問字符串得到的是「字節」。

按字節遍歷

字符串可以通過下標來訪問內部字節數組具體位置上的字節,字節是 byte 類型

<code>package main

import "fmt"

func main() {
\tvar s = "嘻哈china"
\tfor i:=0;i\t\tfmt.Printf("%x ", s[i])
\t}

}

-----------
e5 98 bb e5 93 88 63 68 69 6e 61
複製代碼
/<code>

按字符 rune 遍歷

<code>package main

import "fmt"

func main() {
\tvar s = "嘻哈china"
\tfor codepoint, runeValue := range s {
\t\tfmt.Printf("%d %d ", codepoint, int32(runeValue))

\t}
}

-----------
0 22075 3 21704 6 99 7 104 8 105 9 110 10 97
複製代碼/<code>

對字符串進行 range 遍歷,每次迭代出兩個變量 codepoint 和 runeValue。codepoint 表示字符起始位置,runeValue 表示對應的 unicode 編碼(類型是 rune)。

字節串的內存表示

如果字符串僅僅是字節數組,那字符串的長度信息是怎麼得到呢?要是字符串都是字面量的話,長度尚可以在編譯期計算出來,但是如果字符串是運行時構造的,那長度又是如何得到的呢?

<code>var s1 = "hello" // 靜態字面量
var s2 = ""
for i:=0;i<10;i++ {
s2 += s1 // 動態構造
}
fmt.Println(len(s1))
fmt.Println(len(s2))
複製代碼/<code>

為解釋這點,就必須瞭解字符串的內存結構,它不僅僅是前面提到的那個字節數組,編譯器還為它分配了頭部字段來存儲長度信息和指向底層字節數組的指針,圖示如下,結構非常類似於切片,區別是頭部少了一個容量字段。


「快學 Go 語言」第 7 課——字符串


當我們將一個字符串變量賦值給另一個字符串變量時,底層的字節數組是共享的,它只是淺拷貝了頭部字段。

字符串是隻讀的

你可以使用下標來讀取字符串指定位置的字節,但是你無法修改這個位置上的字節內容。如果你嘗試使用下標賦值,編譯器在語法上直接拒絕你。

<code>package main

func main() {
\tvar s = "hello"
\ts[0] = 'H'
}
--------
./main.go:5:7: cannot assign to s[0]
複製代碼/<code>

切割切割

字符串在內存形式上比較接近於切片,它也可以像切片一樣進行切割來獲取子串。子串和母串共享底層字節數組。

<code>package main

import "fmt"

func main() {
\tvar s1 = "hello world"
\tvar s2 = s1[3:8]
\tfmt.Println(s2)
}

-------
lo wo

複製代碼/<code>

字節切片和字符串的相互轉換

在使用 Go 語言進行網絡編程時,經常需要將來自網絡的字節流轉換成內存字符串,同時也需要將內存字符串轉換成網絡字節流。Go 語言直接內置了字節切片和字符串的相互轉換語法。

<code>package main 


import "fmt"

func main() {
\tvar s1 = "hello world"
\tvar b = []byte(s1) // 字符串轉字節切片
\tvar s2 = string(b) // 字節切片轉字符串
\tfmt.Println(b)
\tfmt.Println(s2)
}

--------
[104 101 108 108 111 32 119 111 114 108 100]
hello world
複製代碼/<code>

從節省內存的角度出發,你可能會認為字節切片和字符串的底層字節數組是共享的。但是事實不是這樣的,底層字節數組會被拷貝。如果內容很大,那麼轉換操作是需要一定成本的。

那為什麼需要拷貝呢?因為字節切片的底層數組內容是可以修改的,而字符串的底層字節數組是隻讀的,如果共享了,就會導致字符串的只讀屬性不再成立。


分享到:


相關文章: