Go 底層探索 -二-: 字符串
- 介紹
@注: 以下內容來自《Go 語言底層原理剖析》書中的摘要信息,本人使用版本 (
Go1.18
) 與書中不一致,源碼路徑可能會有出入。
字符串在編程語言中無處不在,程序的源文件本身就是由衆多字符組成的,在程序開發中的存儲、傳輸、日誌打印等環節,都離不開字符串的顯示、表達及處理。因此,字符與字符串是編程中最基礎的學問。不同的語言對於字符串的結構、處理有所差異。
1.1 字符串長度
在編程語言中,字符串是一種重要的數據結構,通常由一系列字符組成。字符串一般有兩種類型:
-
一種: 在編譯時指定長度,不能修改。
-
另一種: 具有動態的長度,可以修改。
在 Go 語言中,字符串不能被修改,只能被訪問。
錯誤示例:
var str = "hello word" // 這裏想把e改成o,不支持 str[1]='o'
1.2 字符串的終止方式
字符串的終止有兩種方式:
-
一種是
C
語言中的隱式申明,以字符“\0”
作爲終止符。 -
一種是
Go
語言中的顯式聲明。
- 結構 & 內存
2.1 數據結構
Go
語言運行時字符串string
的表示結構如下:
type StringHeader struct {
Data uintptr
Len int
}
-
Data
: 指向底層的字符數組。 -
Len
: 代表字符串的長度。
字符串在本質上是一串字符數組。
2.2 內存佔用
Go
語言中所有的文件都採用UTF-8
的編碼方式,同時字符常量使用UTF-8
的字符編碼集。
UFT-8
是一種長度可變的編碼方式,可包含世界上大部分的字符。在UTF-8
中,大部分字符都只佔據1
字節,但是特殊的字符(例如大部分中文)會佔據 3 字節。
下面示例: 變量str
看起來只有4
個字符,但是len(str
)獲取的長度爲8
,字符串str
中每個中文都佔據了3
字節。
func TestRun(t *testing.T) {
str := "Go語言"
fmt.Println("str 長度:", len(str))
}
/**
str 長度: 8
*/
- 字符串解析
3.1 解析源碼
字符串常量在詞法解析階段最終會被標記成StringLit
類型的Token
並被傳遞到編譯的下一個階段。在語法分析階段,採取遞歸下降的方式讀取Uft-8
字符,單撇號或雙引號是字符串的標識。
go1.18/src/cmd/compile/internal/syntax/scanner.go:88
func (s *scanner) next() {
...
switch s.ch {
case -1:
...
case '"':
s.stdString()
case '`':
s.rawString()
...
}
根據上面解析源碼可以得知:
-
如果在代碼中識別到單撇號,則調用
rawString
函數; -
如果識別到雙引號,則調用
stdString
函數;
rawString
函數和stdString
函數對字符串的解析處理也略有不同;
3.2 解析函數: rawString
對於單撇號的處理比較簡單:一直循環向後讀取,直到尋找到配對的單撇號,源碼如下:
go1.18/src/cmd/compile/internal/syntax/scanner.go:706
func (s *scanner) rawString() {
ok := true
s.nextch()
for {
if s.ch == '`' {
s.nextch()
break
}
if s.ch < 0 {
s.errorAtf(0, "string not terminated")
ok = false
break
}
s.nextch()
}
s.setLit(StringLit, ok)
}
3.3 解析函數: stdString
雙引號調用stdString
函數,如果出現另一個雙引號則直接退出;如果出現了\\
,則對後面的字符進行轉義。
go1.18/src/cmd/compile/internal/syntax/scanner.go:674
func (s *scanner) stdString() {
ok := true
s.nextch()
for {
if s.ch == '"' {
s.nextch()
break
}
if s.ch == '\\' {
s.nextch()
if !s.escape('"') {
ok = false
}
continue
}
// 雙引號中不能出現換行符
if s.ch == '\n' {
s.errorf("newline in string")
ok = false
break
}
if s.ch < 0 {
s.errorAtf(0, "string not terminated")
ok = false
break
}
s.nextch()
}
s.setLit(StringLit, ok)
}
@注: 在雙引號中不能出現換行符,以下代碼在編譯時會報錯:newline in string。這是通過對每個字符判斷 r=='\n'實現的。
- 字符串拼接
4.1 非運行時拼接
在Go
語言中,可以方便地通過加號操作符(+)
對字符串進行拼接。如下代碼:
func main(){
str := "hello " + "world"
}
由於數字的加法操作也使用+
操作符,因此需要編譯時識別具體爲何種操作:
-
在抽象語法樹階段: 當加號操作符兩邊是字符串時, 具體操作的
Op
會被解析爲OADDSTR
; -
在語法分析階段: 調用
noder.sum
函數, 將所有的字符串常量放到字符串數組中,然後調用strings.Join
函數完成對字符串常量數組的拼接。
4.2 運行時拼接
運行時字符串的拼接原理如圖 5-1 所示,其並不是簡單地將一個字符串合併到另一個字符串中,而是找到一個更大的空間,並通過內存複製的形式將字符串複製到其中。
拼接後的字符串大於或小於 32 字節時的操作:
當拼接後的字符串小於 32 字節時,會有一個臨時的緩存供其使用。
當拼接後的字符串大於 32 字節時,堆區會開闢一個足夠大的內存空間,並將多個字符串存入其中,期間會涉及內存的複製.
- 與字節數組轉換
字節數組與字符串可以相互轉換。如下所示,字符串a
強制轉換爲字節數組b
,字節數組b
強制轉換爲字符串c
。
a := "hello go"
// a強制轉換爲字節數組b
b := []byte(a)
// 字節數組b強制轉換爲字符串c
c := string(b)
5.1 注意事項
字節數組與字符串的相互轉換並不是簡單的指針引用,而是涉及了內存複製;
-
當字符串小於
32
字節時: 可以直接使用緩存buf
; -
當字符串大於
32
字節時: 需要向堆區申請足夠的內存空間。最後使用copy
函數完成內存複製。
@注: 在涉及一些密集的轉換場景時,需要評估這種轉換帶來的性能損耗。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/lv3DGbADT6G9b9Az7LhYWg