Go 底層探索 -二-: 字符串

  1. 介紹

@注: 以下內容來自《Go 語言底層原理剖析》書中的摘要信息,本人使用版本 (Go1.18) 與書中不一致,源碼路徑可能會有出入。

字符串在編程語言中無處不在,程序的源文件本身就是由衆多字符組成的,在程序開發中的存儲、傳輸、日誌打印等環節,都離不開字符串的顯示、表達及處理。因此,字符與字符串是編程中最基礎的學問。不同的語言對於字符串的結構、處理有所差異。

1.1 字符串長度

在編程語言中,字符串是一種重要的數據結構,通常由一系列字符組成。字符串一般有兩種類型:

在 Go 語言中,字符串不能被修改,只能被訪問。

錯誤示例:

var str = "hello word"
// 這裏想把e改成o,不支持
str[1]='o'

1.2 字符串的終止方式

字符串的終止有兩種方式:

  1. 結構 & 內存

2.1 數據結構

Go語言運行時字符串string的表示結構如下:

type StringHeader struct {
 Data uintptr
 Len  int
}

字符串在本質上是一串字符數組。

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
*/
  1. 字符串解析

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函數對字符串的解析處理也略有不同;

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'實現的。

  1. 字符串拼接

4.1 非運行時拼接

Go語言中,可以方便地通過加號操作符(+)對字符串進行拼接。如下代碼:

func main(){
  str := "hello " + "world"
}

由於數字的加法操作也使用+操作符,因此需要編譯時識別具體爲何種操作:

4.2 運行時拼接

運行時字符串的拼接原理如圖 5-1 所示,其並不是簡單地將一個字符串合併到另一個字符串中,而是找到一個更大的空間,並通過內存複製的形式將字符串複製到其中。

拼接後的字符串大於或小於 32 字節時的操作:

  • 當拼接後的字符串小於 32 字節時,會有一個臨時的緩存供其使用。

  • 當拼接後的字符串大於 32 字節時,堆區會開闢一個足夠大的內存空間,並將多個字符串存入其中,期間會涉及內存的複製.

  1. 與字節數組轉換

字節數組與字符串可以相互轉換。如下所示,字符串a強制轉換爲字節數組b,字節數組b強制轉換爲字符串c

a := "hello go"
// a強制轉換爲字節數組b
b := []byte(a)
// 字節數組b強制轉換爲字符串c
c := string(b)

5.1 注意事項

字節數組與字符串的相互轉換並不是簡單的指針引用,而是涉及了內存複製

@注: 在涉及一些密集的轉換場景時,需要評估這種轉換帶來的性能損耗。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/lv3DGbADT6G9b9Az7LhYWg