Skip to content

Go字符串

在 Go 中,字符串本質上是一個不可變的、只讀的字節序列(byte sequence),這裡「字節序列」指的是字符串的底層數據由一串按順序排列的字節組成,這些字節佔用一片連續的內存空間。

字面量

前面提到過字符串有兩種字面量表達方式,分為普通字符串和原生字符串。

普通字符串

普通字符串由""雙引號表示,支持轉義,不支持多行書寫,下列是一些普通字符串

go
"這是一個普通字符串\n"
"abcdefghijlmn\nopqrst\t\\uvwxyz"
這是一個普通字符串
abcdefghijlmn
opqrst  \uvwxyz

原生字符串

原生字符串由反引號表示,不支持轉義,支持多行書寫,原生字符串裡面所有的字符都會原封不動的輸出,包括換行和縮進。

go
`這是一個原生字符串,換行
  tab縮進,\t制表符但是無效,換行
  "這是一個普通字符串"

  結束
`
這是一個原生字符串,換行
        tab縮進,\t制表符但是無效,換行
        "這是一個普通字符串"

        結束

訪問

因為字符串本質是字節序列,其索引操作str[i]被設計為返回第 i 個字節,語法上與切片一致,例如訪問字符串第一個元素

go
func main() {
   str := "this is a string"
   fmt.Println(str[0])
}

輸出是字節編碼值而不是字符

116

切割字符串

go
func main() {
   str := "this is a string"
   fmt.Println(string(str[0:4]))
}
this

嘗試修改字符串元素

go
func main() {
   str := "this is a string"
   str[0] = 'a' // 無法通過編譯
   fmt.Println(str)
}
main.go:7:2: cannot assign to str[0] (value of type byte)

雖然沒法修改字符串,但是可以覆蓋

go
func main() {
   str := "this is a string"
   str = "that is a string"
   fmt.Println(str)
}
that is a string

轉換

字符串可以轉換為字節切片,而字節切片或字節序列也可以轉換為字符串,例子如下:

go
func main() {
   str := "this is a string"
   // 顯式類型轉換為字節切片
   bytes := []byte(str)
   fmt.Println(bytes)
   // 顯式類型轉換為字符串
   fmt.Println(string(bytes))
}

字符串的內容是只讀的不可變的,無法修改,但是字節切片是可以修改的。

go
func main() {
  str := "this is a string"
  fmt.Println(&str)
  bytes := []byte(str)
    // 修改字節切片
  bytes = append(bytes, 96, 97, 98, 99)
    // 賦值給原字符串
  str = string(bytes)
  fmt.Println(str)
}

將字符串轉換成字節切片以後,兩者之間毫無關聯,因為 Go 會新分配一片內存空間給字節切片,再將字符串的內存復制過去,對字節切片進行修改不會對原字符串產生任何影響,這麼做是為了內存安全。

在這種情況下,如果要轉換的字符串或字節切片很大,那麼性能開銷就會很高。不過你也可以通過unsafe庫來實現無復制轉換,不過背後的安全問題需要自己承擔,比如下面的例子,b1 和 s1 的地址是一樣的。

go
func main() {
  s1 := "hello world"
  b1 := unsafe.Slice(unsafe.StringData(s1), len(s1))
  fmt.Printf("%p %p", unsafe.StringData(s1), unsafe.SliceData(b1))
}
0xe27bb2 0xe27bb2

長度

字符串的長度,其實並不是字符的個數,而是字節序列的長度。只是大多數時候我們處理的是 ASCII 字符,每個字符恰好能用一個字節表示,所以字節長度與字符個數恰好相等。求字符串長度使用內置函數len,例子如下:

go
func main() {
   str := "this is a string" // 看起來長度是16
   str2 := "這是一個字符串" // 看起來長度是7
   fmt.Println(len(str), len(str2))
}
16 21

看起來中文字符串比英文字符串短,但是實際求得的長度卻比英文字符串長。這是因為在unicode編碼中,一個漢字在大多數情況下佔 3 個字節,一個英文字符只佔一個字節,通過輸出字符串第一個元素可以看出結果:

go
func main() {
   str := "this is a string"
   str2 := "這是一個字符串"
   fmt.Println(string(str[0]))
   fmt.Println(string(str2[0]))
   fmt.Println(string(str2[0:3]))
}
t // 字母t
è // 一個中文字符的「碎片」(第一個字節)的編碼值,碰巧與意大利語字符 è 的編碼值相同
這 // 中文漢字

拷貝

類似數組切片的拷貝方式,字符串拷貝其實是字節切片拷貝,使用內置函數copy

go
func main() {
   var dst, src string
   src = "this is a string"
   desBytes := make([]byte, len(src))
   copy(desBytes, src)
   dst = string(desBytes)
   fmt.Println(src, dst)
}

也可以使用strings.clone函數,但其實內部實現都差不多

go
func main() {
   var dst, src string
   src = "this is a string"
   dst = strings.Clone(src)
   fmt.Println(src, dst)
}

拼接

字符串的拼接使用+操作符

go
func main() {
   str := "this is a string"
   str = str + " that is a int"
   fmt.Println(str)
}

也可以轉換為字節切片再進行添加元素

go
func main() {
   str := "this is a string"
   bytes := []byte(str)
   bytes = append(bytes, "that is a int"...)
   str = string(bytes)
   fmt.Println(str)
}

以上兩種拼接方式性能都很差,一般情況下可以使用,但如果對應性能有更高要求,可以使用strings.Builder

go
func main() {
   builder := strings.Builder{}
   builder.WriteString("this is a string ")
   builder.WriteString("that is a int")
   fmt.Println(builder.String())
}
this is a string that is a int

遍歷

在本文開頭就已經提到過,Go 中的字符串就是一個只讀的字節切片,也就是說字符串的組成單位是字節而不是字符。這種情況經常會在遍歷字符串時遇到,例如下方的代碼

go
func main() {
  str := "hello world!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

例子中分別輸出了字節的十進制形式和十六進制形式。

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
119,77,w
111,6f,o
114,72,r
108,6c,l
100,64,d
33,21,!

由於例子中的字符都是屬於 ASCII 字符,只需要一個字節就能表示,所以結果恰巧每一個字節對應一個字符。但如果包含非 ASCII 字符結果就不同了,如下

go
func main() {
  str := "hello 世界!"
  for i := 0; i < len(str); i++ {
    fmt.Printf("%d,%x,%s\n", str[i], str[i], string(str[i]))
  }
}

通常情況下,一個中文字符會佔用 3 個字節,所以就可能會看到以下結果

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
228,e4,ä
184,b8,¸
150,96,–
231,e7,ç
149,95,•
140,8c,Œ
33,21,!

按照字節來遍歷會把中文字符拆開,這顯然會出現亂碼。Go 字符串是明確支持 UTF-8 的,應對這種情況就需要用到rune類型,在使用for range進行遍歷時,其默認的遍歷單位類型就是一個rune,例如下方代碼

go
func main() {
   str := "hello 世界!"
   for _, r := range str {
      fmt.Printf("%d,%x,%s\n", r, r, string(r))
   }
}

輸出如下

104,68,h
101,65,e
108,6c,l
108,6c,l
111,6f,o
32,20,
19990,4e16,世
30028,754c,界
33,21,!

rune本質上是int32的類型別名,unicode 字符集的范圍位於 0x0000 - 0x10FFFF 之間,最大也只有三個字節,合法的 UTF-8 編碼最大字節數只有 4 個字節,所以使用int32來存儲是理所當然,上述例子中將字符串轉換成[]rune再遍歷也是一樣的道理,如下

go
func main() {
   str := "hello 世界!"
   runes := []rune(str)
   for i := 0; i < len(runes); i++ {
      fmt.Println(string(runes[i]))
   }
}

還可以使用utf8包下的工具,例如

go
func main() {
  str := "hello 世界!"
  for i, w := 0, 0; i < len(str); i += w {
    r, width := utf8.DecodeRuneInString(str[i:])
    fmt.Println(string(r))
    w = width
  }
}

這兩個例子的輸出都是相同的。

TIP

關於字符串的更多細節,可以前往Strings, bytes, runes and characters in Go了解。

Golang學習網由www.golangdev.cn整理維護