深入Go語言文本類型
Go的作者Ken Thompson是UTF-8的發明人(也是C,Unix,Plan9等的創始人),因此在關於字符編碼上,Go有著獨到而周全的設計。本文介紹了Go語言中的三種內置文本類型:string
, byte
,rune
的內部表示與相互轉換。
1. 概覽
Go中,字符串string
是內置類型,與文本處理相關的內置類型還有符文rune
和字節byte
。
UTF-8編碼在Go語言中有著特殊的位置,無論是源代碼的文本編碼,還是字符串的內部編碼都是UTF-8。Go繞開前輩語言們踩過的坑,使用了UTF8作為默認編碼是一個非常明智的選擇。相比之下,Java,Javascript都使用 UCS-2/UTF16作為內部編碼,早期還有隨機訪問的優勢,可當Unicode增長超出BMP之後,這一優勢也蕩然無存了。相比之下,字節序,Surrogate , 空間冗餘帶來的麻煩卻仍讓人頭大無比。
標準庫
與C語言類似,大多數關於字符串處理的函數都放在標準庫裏。Go將大部分字符串處理的函數放在了strings
,bytes
這兩個包裏。因為在字符串和整型間沒有隱式類型轉換,字符串和其他基本類型的轉換的功能主要在標準庫strconv
中提供。unicode
相關功能在unicode
包中提供。encoding
包提供了一係列其他的編碼支持。
摘要
- Go語言源代碼總是采用
UTF-8
編碼 - 字符串
string
可以包含任意字節序列,通常是UTF-8
編碼的。 - 字符串字麵值,在不帶有字節轉義的情況下一定是
UTF-8
編碼的。 - Go使用
rune
代表Unicode
**碼位**。一個**字符**可能由一個或多個碼位組成(複合字符) - Go string是建立在**字節數組**的基礎上的,因此對string使用
[]
索引會得到字節byte
而不是字符rune
。 - Go語言的字符串不是正規化(
normalized
)的,因此同一個字符可能由不同的字節序列表示。使用unicode/norm
解決此類問題。
基礎數據結構
數組與切片
要討論[]byte
和[]rune
,就必需先解釋Go語言中的**數組(Array)**與**切片(Slice)**,數組很好理解,和C語言中的數組概念一致,**切片**則是對**數組**的引用。
數組Array
是固定長度的數據結構,不存放任何額外的信息。很少直接使用,往往用作切片的底層存儲。
切片Slice
描述了數組中一個連續的片段,Go語言的切片操作與Python較為類似。在底層實現中,切片可以看成一個由三個word組成的結構體,這裏word是CPU的字長。這三個字分別是ptr
,len
,cap
,分別代表數組首元素地址,切片的長度,當前切片頭位置到底層數組尾部的距離。
因此,在函數參數中傳遞十個元素的數組,那麼就會在棧上複製這十個元素。而傳遞一個切片,則實際上傳遞的是這個3Word結構體。傳遞切片本身就是傳遞引用。
字節byte
字節byte
實際上是uint8
的別名,隻是為了和其他8bit類型相區別才單獨起了別名。通常出現的更多的是字節切片[]byte
與字節數組[...]byte
。
字麵值
字節可以用單引號擴起的單個字符表示,不過這種字麵值和rune
的字麵值很容易搞混。賦予字節變量一個超出範圍的值,如果在編譯期能檢查出來就會報overflows byte
編譯錯誤。
底層結構
對於字節數組[]byte
,實質上可以看做[]uint8
,即一個整形切片,所以字節數組的本體結構定義如下:
type SliceHeader struct {
Data uintptr
Len int
Cap int
}
字符串string
字符串通常是UTF8編碼的文本,由一係列8bit字節組成。raw string literal
和不含轉義符號的string literal
一定是UTF-8
編碼的,但string
其實可以含有任意的字節序列。
字符串是不可變對象,可以空(s=""
),但不會是nil
。
底層結構
string
在Go中的實現與Slice
類似,但因為字符串是不可變類型,因此底層數組的長度就是字符串的長度,所以相比切片,string
結構的本體少了一個Cap
字段。隻有一個指針和一個長度值,由兩個Word組成。64位機器上占用16個字節。
type StringHeader struct {
Data uintptr
Len int
}
雖然字符串是不可變類型,但通過指針和強製轉換,還是可以進行一些危險但高效的操作的。不過要注意,編譯器作為常量確定的string
會寫入隻讀段,是不可以修改的。相比之下,fmt.Sprintf
生成的字符串分配在堆上,就可以通過黑魔法進行修改。
關於string
,有這麼幾點需要注意。
-
string
常量會在編譯期分配到**隻讀段**,對應數據地址不可寫入。 - 相同的
string
常量不會重複存儲,但動態生成的字符串即使內容一樣,數據也是在不同的空間。 - 常量空字符串有數據地址,動態生成的字符串沒有設置數據地址 ,隻有動態生成的string可以unsafe魔改。
- Golang string和[]byte轉換,會將數據複製到堆上,返回數據指向複製的數據。所以string(bytes)存在開銷
- string和[]byte通過複製轉換,性能損失接近4倍
符文rune
符文rune
其實是int32
的別名,表示一個Unicode的**碼位**。
注意一個**字符(Character)**可以由一個或多個**碼位(Code Point)**構成。例如帶音調的e
,即é
,既可以由\u00e9
單個碼位表示,也可以由e
和口音符號\u0301
複合而成。這涉及到normalization的問題。但通常情況下一個字符就是一個碼位。
>>> print u'\u00e9', u'e\u0301',u'e\u0301\u0301\u0301'
é é é́́
符文的字麵值是用單引號括起的一個或多個字符,例如a
,啊
,\a
,\141
,\x61
,\u0061
,\U00000061
,都是合法的rune literal。其格式定義如下:
rune_lit = "'" ( unicode_value | byte_value ) "'" .
unicode_value = unicode_char | little_u_value | big_u_value | escaped_char .
byte_value = octal_byte_value | hex_byte_value .
octal_byte_value = `\` octal_digit octal_digit octal_digit .
hex_byte_value = `\` "x" hex_digit hex_digit .
little_u_value = `\` "u" hex_digit hex_digit hex_digit hex_digit .
big_u_value = `\` "U" hex_digit hex_digit hex_digit hex_digit
hex_digit hex_digit hex_digit hex_digit .
escaped_char = `\` ( "a" | "b" | "f" | "n" | "r" | "t" | "v" | `\` | "'" | `"` ) .
其中,八進製的數字範圍是0~255,Unicode轉義字符通常要排除0x10FFFF以上的字符和surrogate字符。
看上去這樣用單引號括起來的字麵值像是一個字符串,但當源代碼轉換為內部表示時,它其實就是一個int32
。所以var b byte = '蛤'
,其實就是為uint8
賦了一個int32
的值,會導致溢出。相應的,一個rune
也可以在不產生溢出的條件下賦值給byte
。
文本類型轉換
三種基本文本類型之間可以相互轉換,當然,有常規的做法,也有指針黑魔法。
string
與[]byte
的轉換
string
和bytes
的轉換是最常見的,因為通常通過IO得到的都是[]byte
,例如io.Reader
接口的方法簽名為:Read(p []byte) (n int, err error)
。但日常字符串操作使用的都是string
,這就需要在兩者之間進行轉換。
常規做法
通常[]byte
和string
可以直接通過類型名強製轉化,但實質上執行了一次堆複製。理論上stringHeader
隻是比sliceHeader
少一個cap
字段,但因為string
需要滿足不可變的約束,而[]byte
是可變的,因此在執行[]byte
到string
的操作時會進行一次複製,在堆上新分配一次內存。
// byte to string
s := string(b)
// string index -> byte
s[i] = b
// []byte to string
s := string(bytes)
// string to []byte
bytes := []byte(s)
黑魔法
利用unsafe.Pointer
和reflect
包可以實現很多禁忌的黑魔法,但這些操作對GC並不友好。最好不要嚐試。
type Bytes []byte
// 將string轉換為[]byte,'可以修改',很危險,因為[]byte結構要多一個cap字段。
func StringBytes(s string) Bytes {
return *(*Bytes)(unsafe.Pointer(&s))
}
// 不拷貝地將[]byte轉換為string
func BytesString(b []byte) String {
// 因為[]byte的Header隻比string的Header多一個Cap字段。可以直接強製成`*String`
return *(*String)(unsafe.Pointer(&b))
}
// 獲取&s[0],即存儲字符串的字節數組的地址指針,Go裏不允許這種操作。
func StringPointer(s string) unsafe.Pointer {
p := (*reflect.StringHeader)(unsafe.Pointer(&s))
return unsafe.Pointer(p.Data)
}
// r獲取&b[0],即[]byte底層數組的地址指針,Go裏不允許這種操作
func BytesPointer(b []byte) unsafe.Pointer {
p := (*reflect.SliceHeader)(unsafe.Pointer(&b))
return unsafe.Pointer(p.Data)
}
string
與rune
的轉換
string
是UTF8編碼的字符串,因此對於非含有ASCII字符的字符串,是沒法簡單的直接索引的。例如
fmt.Printf("%x","hello"[0])
,會取出第一個字節h的相應字節表示uint8
,值為:0x68。然而
fmt.Printf("%s","你好"[0])
,也是同理,在UTF-8編碼中,漢字"你"被編碼為0xeE4BDA0
由三個字節組成,因此使用下標0去索引字符串,並不會取出第一個漢字字符的int32
碼位值0x4f60
來,而是這三個字節中的第一個0xE4
。
沒有辦法隨機訪問一個中文漢字是一件很蛋疼的事情。曾經Java和Javascript之類的語言就出於性能考慮使用UCS2/UTF-16來平衡時間和空間開銷。但現在Unicode字符遠遠超過65535個了,這點優勢已經蕩然無存,想要準確的索引一個字符(尤其是帶Emoji的),也需要用特製的API從頭解碼啦,啪啪啪打臉蒼天饒過誰……。
常規方式
string
和rune
之間也可以通過類型名直接轉換,不過string
不能直接轉換成單個的rune
。
// rune to string
str := string(r)
// range string -> rune
for i,r := range str
// string to []rune
runes := []rune(str)
// []rune to string
str := string(runes)
特殊支持
Go對於UTF-8
有特殊的支持和處理(因為UTF-8
和Go
都是Ken
發明的……。),這體現在對於string
的range
迭代上。
const nihongo = "日本語"
for index, runeValue := range nihongo {
fmt.Printf("%#U starts at byte position %d\n", runeValue, index)
}
U+65E5 '日' starts at byte position 0
U+672C '本' starts at byte position 3
U+8A9E '語' starts at byte position 6
直接索引string
會得到字節序號和相應字節。而對string
進行range
迭代,獲得的就是字符rune
的索引與相應的rune
。
byte
與rune
的轉換
byte
其實是uint8
,而rune
實際就是int32
,所以uint8
和int32
兩者之間的轉換就是整數的轉換。
但是[]uint8
和[]int32
是兩個不同類型的整形數組,它們之間是沒有直接強製轉換的方法的,好在通過string
來曲線救國:runes := []rune(string(bytes))
最後更新:2017-08-13 22:23:22