- Go語言高級編程(第2版)
- 柴樹杉 曹春暉
- 1823字
- 2025-08-07 17:56:12
1.3.2 字符串
一個字符串是一個不可變的字節序列。字符串通常用來存儲人類可讀的文本數據。與數組不同,字符串的元素不可修改,因此字符串類似于一個只讀的字節數組。雖然每個字符串的長度是固定的,但是長度并不是字符串類型的一部分。由于Go語言的源文件要求使用UTF-8編碼,因此Go源文件中出現的字符串字面值常量一般也是UTF-8編碼的。源文件中的文本字符串通常被解釋為采用UTF-8編碼的Unicode碼點(rune)序列。因為字節序列對應的是二進制字節序列,所以字符串可以包含任意的數據,包括字節值0。我們也可以用字符串表示GBK等非UTF-8編碼的數據,不過這時候將字符串看作是一個只讀的二進制數組更準確,因為for range
等語法并不能支持非UTF-8編碼的字符串的遍?歷。
Go語言字符串的底層結構在reflect.
StringHeader
中定義:
type StringHeader struct { Data uintptr Len int }
字符串結構由兩個信息組成:第一個是指向字符串底層的字節數組的地址;第二個是字符串底層的字節數組長度。字符串其實是一個結構體,因此字符串的賦值操作也就是reflect.
StringHeader
結構體的復制過程,并不會涉及底層字節數組的復制。1.3.1節中提到的[2]string
字符串數組對應的底層結構和[2]reflect.
StringHeader
對應的底層結構是一樣的,可以將字符串數組看作一個結構體數?組。
我們可以看看字符串"hello, world"
的內存結構,如圖1-7所?示。

圖1-7 字符串"hello, world"
的
內存結構
分析可以發現,字符串"hello, world"
的
底層數據和以下數組是完全一致的:
var data = [...]byte{ 'h', 'e', 'l', 'l', 'o', ',', ' ', 'w', 'o', 'r', 'l', 'd', }
字符串雖然不是切片,但是支持切片操作,不同位置的切片底層訪問的是同一塊內存數據(因為字符串是只讀的,所以相同的字符串字面值常量通常對應同一個字符串常量):
s := "hello, world" hello := s[:5] world := s[7:] s1 := "hello, world"[:5] s2 := "hello, world"[7:]
字符串和數組類似,內置的len()
函數返回字符串的長度。也可以通過reflect. S
tringHeader
結構訪問字符串的長度(這里只是為了演示字符串的結構,并不是推薦的做法):
fmt.Println("len(s):", (*reflect.StringHeader)(unsafe.Pointer(&s)).Len) // 12 fmt.Println("len(s1):", (*reflect.StringHeader)(unsafe.Pointer(&s1)).Len) // 5 fmt.Println("len(s2):", (*reflect.StringHeader)(unsafe.Pointer(&s2)).Len) // 5
根據Go語言規范,Go語言的源文件都采用UTF-8編碼。因此,Go源文件中出現的字符串字面值常量一般也是UTF-8編碼的(對于轉義字符,則沒有這個限制)。提到Go字符串時,一般都會假設字符串對應的是一個合法的UTF-8編碼的字節序列。可以用內置的print
調試函數或fmt
.Print()
函數直接打印,也可以用for range
循環直接遍歷UTF-8解碼后的Unicode碼點?值。
下面的"hello,
世界"
字符串中包含了中文字符,可以通過打印對應的字節切片來查看字符底層對應的數據:
fmt.Printf("%#v\n", []byte("hello, 世界"))
輸出的結果是:
[]byte{0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xe4, 0xb8, 0x96, 0xe7, \ 0x95, 0x8c}
分析結果可以發現,0xe4, 0xb8, 0x96
對應中文“世”,0xe7, 0x95, 0x8c
對應中文“界”。我們也可以在字符串字面值中直接指定UTF-8編碼后的值(源文件中全部是ASCII碼,可以避免出現多字節的字符)。
fmt.Println("\xe4\xb8\x96") // 打印“世” fmt.Println("\xe7\x95\x8c") // 打印“界”
圖1-8展示了字符串"
hello, 世界"
的內存結?構。

圖1-8 字符串"hello,
世界
"
的
內存結構
Go語言的字符串中可以存放任意的二進制字節序列。即使是UTF-8字節序列,也可能會遇到錯誤的編碼。如果遇到一個錯誤的UTF-8編碼輸入,將生成一個特別的Unicode字符,這個字符在不同的軟件中的顯示效果可能不太一樣,在印刷中,這個字符通常是一個黑色六角形或鉆石形狀,里面包含一個白色的問號,即“?”。
在下面的字符串中,我們故意損壞了第一字符的第二和第三字節,因此第一字符將會打印為“?”,第二和第三字節則被忽略,后面的“abc”依然可以正常解碼打印(錯誤編碼不會向后擴散是UTF-8編碼的優秀特性之一)。
fmt.Println("\xe4\x00\x00\xe7\x95\x8cabc") // ?界abc
在用for range
循環
遍歷這個損壞的UTF-8字符串時,第一字符的第二和第三字節依然會被單獨遍歷到,不過此時得到的值是損壞后的0:
for i, c := range "\xe4\x00\x00\xe7\x95\x8cabc" { fmt.Println(i, c) } // 0 65533 // \uFFF,對應? // 1 0 // 空字符 // 2 0 // 空字符 // 3 30028 // 界 // 6 97 // a // 7 98 // b // 8 99 // c
如果不想解碼UTF-8字符串,而想直接遍歷原始的字節碼,可以將字符串強制轉換為[]byte
字節序列后再進行遍歷(這里的轉換一般不會產生運行時開銷):
for i, c := range []byte("世界abc") { fmt.Println(i, c) }
或者采用傳統的下標方式遍歷字符串的字節數組:
const s = "\xe4\x00\x00\xe7\x95\x8cabc" for i := 0; i < len(s); i++ { fmt.Printf("%d %x\n", i, s[i]) }
Go語言除了for range
語法對UTF-8字符串提供了特殊支持外,還對字符串和[]rune
類型的相互轉換提供了特殊的支?持。
fmt.Printf("%#v\n", []rune("世界")) // []int32{19990, 30028} fmt.Printf("%#v\n", string([]rune{'世', '界'})) // 世界
從上面代碼的輸出結果可以發現,[]rune
其實是[]int32
類型,這里的rune
只是int32
類型的別名,并不是重新定義的類型。rune
用于表示每個Unicode碼點,目前只使用了21位。
字符串相關的強制類型轉換主要涉及[]byte
和[]rune
兩種類型。每種轉換都可能隱含重新分配內存的代價,在最壞的情況下,它們運算的時間復雜度都是O
(
n
)
。不過字符串和[]rune
的轉換要更為特殊一些,因為通常的強制類型轉換要求兩個類型的底層內存結構要盡量一致,但字符串和[]rune
底層對應的[]byte
和[]int32
類型是完全不同的內存結構,因此這種轉換可能隱含重新分配內存的操?作。
下面分別用偽代碼簡單模擬Go語言對字符串內置的一些操作,這樣對每個操作的時間復雜度和空間復雜度都會有較明確的認?識。
(1)for range
對字符串的遍歷模擬實現如下:
func forOnString(s string, forBody func(i int, r rune)) { for i := 0; len(s) > 0; { r, size := utf8.DecodeRuneInString(s) forBody(i, r) s = s[size:] i += size } }
用
for range
循環
遍歷字符串時,每次解碼一個Unicode字符,然后進入for
循環體,遇到損壞的編碼并不會導致循環停?止。
(2)[]byte(s)
轉換模擬實現如下:
func str2bytes(s string) []byte { p := make([]byte, len(s)) for i := 0; i < len(s); i++ { c := s[i] p[i] = c } return p }
模擬實現中新創建了一個切片,然后將字符串的數組逐一復制到切片中,這是為了保證字符串只讀的語義。當然,在將字符串轉換為[]byte
時,如果轉換后的變量沒有被修改,編譯器可能會直接返回原始的字符串對應的底層數?據。
(3)string(bytes)
轉換模擬實現如下:
func bytes2str(s []byte) (p string) { data := make([]byte, len(s)) for i, c := range s { data[i] = c } hdr := (*reflect.StringHeader)(unsafe.Pointer(&p)) hdr.Data = uintptr(unsafe.Pointer(&data[0])) hdr.Len = len(s) return p }
因為Go語言的字符串是只讀的,無法以直接構造底層字節數組的方式生成字符串。在模擬實現中通過unsafe
包獲取字符串的底層數據結構,然后將切片的數據逐一復制到字符串中,這同樣是為了保證字符串只讀的語義不受切片的影響。如果轉換后的字符串在生命周期中原始的[]byte
的變量不發生變化,編譯器可能會直接基于[]byte
底層的數據構建字符?串。
(4)[]rune(s)
轉換模擬實現如下:
func str2runes(s string) []rune { var p []int32 for len(s) > 0 { r, size := utf8.DecodeRuneInString (s) p = append(p, int32(r)) s = s[size:] } return []rune(p) }
因為底層內存結構的差異,所以字符串到[]rune
的轉換必然會導致重新分配[]rune
內存空間,然后依次解碼并復制對應的Unicode碼點值。這種強制轉換并不存在前面提到的字符串和字節切片轉換時的優化情?況。
(5)string(runes)
轉換模擬實現如下:
func runes2string(s []int32) string { var p []byte buf := make([]byte, 3) for _, r := range s { n := utf8.EncodeRune(buf, r) p = append(p, buf[:n]...) } return string(p) }
同樣因為底層內存結構的差異,[]rune
到字符串的轉換也必然會導致重新構造字符串。這種強制轉換并不存在前面提到的優化情?況。
- 從零開始構建企業級RAG系統
- 垃圾回收的算法與實現
- SQL Server 2016從入門到精通(視頻教學超值版)
- DevOps入門與實踐
- 網店設計看這本就夠了
- Reactive Programming With Java 9
- Serverless computing in Azure with .NET
- Android嵌入式系統程序開發(基于Cortex-A8)
- 原型設計:打造成功產品的實用方法及實踐
- Pandas 1.x Cookbook
- 算法訓練營:海量圖解+競賽刷題(入門篇)
- Apache Kafka 1.0 Cookbook
- 多接入邊緣計算實戰
- C/C++語言程序開發參考手冊
- 邊做邊學深度強化學習:PyTorch程序設計實踐