Go字符串
在 Go 中,字符串本質上是一個不可變的、只讀的字節序列(byte sequence),這裡「字節序列」指的是字符串的底層數據由一串按順序排列的字節組成,這些字節佔用一片連續的內存空間。
字面量
前面提到過字符串有兩種字面量表達方式,分為普通字符串和原生字符串。
普通字符串
普通字符串由""雙引號表示,支持轉義,不支持多行書寫,下列是一些普通字符串
"這是一個普通字符串\n"
"abcdefghijlmn\nopqrst\t\\uvwxyz"這是一個普通字符串
abcdefghijlmn
opqrst \uvwxyz原生字符串
原生字符串由反引號表示,不支持轉義,支持多行書寫,原生字符串裡面所有的字符都會原封不動的輸出,包括換行和縮進。
`這是一個原生字符串,換行
tab縮進,\t制表符但是無效,換行
"這是一個普通字符串"
結束
`這是一個原生字符串,換行
tab縮進,\t制表符但是無效,換行
"這是一個普通字符串"
結束訪問
因為字符串本質是字節序列,其索引操作str[i]被設計為返回第 i 個字節,語法上與切片一致,例如訪問字符串第一個元素
func main() {
str := "this is a string"
fmt.Println(str[0])
}輸出是字節編碼值而不是字符
116切割字符串
func main() {
str := "this is a string"
fmt.Println(string(str[0:4]))
}this嘗試修改字符串元素
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)雖然沒法修改字符串,但是可以覆蓋
func main() {
str := "this is a string"
str = "that is a string"
fmt.Println(str)
}that is a string轉換
字符串可以轉換為字節切片,而字節切片或字節序列也可以轉換為字符串,例子如下:
func main() {
str := "this is a string"
// 顯式類型轉換為字節切片
bytes := []byte(str)
fmt.Println(bytes)
// 顯式類型轉換為字符串
fmt.Println(string(bytes))
}字符串的內容是只讀的不可變的,無法修改,但是字節切片是可以修改的。
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 的地址是一樣的。
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,例子如下:
func main() {
str := "this is a string" // 看起來長度是16
str2 := "這是一個字符串" // 看起來長度是7
fmt.Println(len(str), len(str2))
}16 21看起來中文字符串比英文字符串短,但是實際求得的長度卻比英文字符串長。這是因為在unicode編碼中,一個漢字在大多數情況下佔 3 個字節,一個英文字符只佔一個字節,通過輸出字符串第一個元素可以看出結果:
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
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函數,但其實內部實現都差不多
func main() {
var dst, src string
src = "this is a string"
dst = strings.Clone(src)
fmt.Println(src, dst)
}拼接
字符串的拼接使用+操作符
func main() {
str := "this is a string"
str = str + " that is a int"
fmt.Println(str)
}也可以轉換為字節切片再進行添加元素
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
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 中的字符串就是一個只讀的字節切片,也就是說字符串的組成單位是字節而不是字符。這種情況經常會在遍歷字符串時遇到,例如下方的代碼
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 字符結果就不同了,如下
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,例如下方代碼
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再遍歷也是一樣的道理,如下
func main() {
str := "hello 世界!"
runes := []rune(str)
for i := 0; i < len(runes); i++ {
fmt.Println(string(runes[i]))
}
}還可以使用utf8包下的工具,例如
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了解。
