背景:在咱們使用 Golang 進行開發過程當中,老是繞不開對字符或字符串的處理,而在 Golang 語言中,對字符和字符串的處理方式可能和其餘語言不太同樣,好比 Python 或 Java 類的語言,本篇文章分享一些 Golang 語言下的 Unicode 和字符串編碼。git
注意
: 在 Golang 語言中的標識符能夠包含 "任何 Unicode 編碼能夠標識的字母字符"。github
被轉換的整數值應該能夠表明一個有效的 Unicode 代碼點,不然轉換的結果就將會是 "�",即:一個僅由高亮的問號組成的字符串值。golang
另外,當一個 string 類型的值被轉換爲 [] rune 類型值的時候,其中的字符串會被拆分紅一個一個的 Unicode 字符。web
顯然,Go 語言採用的字符編碼方案從屬於 Unicode 編碼規範。更確切地說,Go 語言的代碼正是由 Unicode 字符組成的。Go 語言的全部源代碼,都必須按照 Unicode 編碼規範中的 UTF-8 編碼格式進行編碼。編輯器
換句話說,Go 語言的源碼文件必須使用 UTF-8 編碼格式進行存儲。若是源碼文件中出現了非 UTF-8 編碼的字符,那麼在構建、安裝以及運行的時候,go 命令就會報告錯誤 "illegal UTF-8 encoding"。ui
ASCII 編碼方案使用單個字節(byte)的二進制數來編碼一個字符。標準的 ASCII 編碼用一個字節的最高比特(bit)位做爲奇偶校驗位,而擴展的 ASCII 編碼則將此位也用於表示字符。ASCII 編碼支持的可打印字符和控制字符的集合也被叫作 ASCII 編碼集。編碼
咱們所說的 Unicode 編碼規範,其實是另外一個更加通用的、針對書面字符和文本的字符編碼標準。它爲世界上現存的全部天然語言中的每個字符,都設定了一個惟一的二進制編碼。spa
它定義了不一樣天然語言的文本數據在國際間交換的統一方式,併爲全球化軟件建立了一個重要的基礎。翻譯
Unicode 編碼規範以 ASCII 編碼集爲出發點,並突破了 ASCII 只能對拉丁字母進行編碼的限制。它不但提供了能夠對世界上超過百萬的字符進行編碼的能力,還支持全部已知的轉義序列和控制代碼。code
咱們都知道,在計算機系統的內部,抽象的字符會被編碼爲整數。這些整數的範圍被稱爲代碼空間。在代碼空間以內,每個特定的整數都被稱爲一個代碼點。
一個受支持的抽象字符會被映射並分配給某個特定的代碼點,反過來說,一個代碼點老是能夠被當作一個被編碼的字符。
Unicode 編碼規範一般使用十六進制表示法來表示 Unicode 代碼點的整數值,並使用 「U+」 做爲前綴。好比,英文字母字符 「a」 的 Unicode 代碼點是 U+0061。在 Unicode 編碼規範中,一個字符能且只能由與它對應的那個代碼點表示。
Unicode 編碼規範如今的最新版本是 11.0,並會於 2019 年 3 月發佈 12.0 版本。而 Go 語言從 1.10 版本開始,已經對 Unicode 的 10.0 版本提供了全面的支持。對於絕大多數的應用場景來講,這已經徹底夠用了。
Unicode 編碼規範提供了三種不一樣的編碼格式,即:UTF-8
、UTF-16
和 UTF-32
。其中的 UTF 是 UCS Transformation Format 的縮寫。而 UCS 又是 Universal Character Set 的縮寫,但也能夠表明 Unicode Character Set。因此,UTF 也能夠被翻譯爲 Unicode 轉換格式。它表明的是字符與字節序列之間的轉換方式。
在這幾種編碼格式的名稱中,「-」 右邊的整數的含義是,以多少個比特位做爲一個編碼單元。以 UTF-8 爲例,它會以 8 個比特,也就是一個字節,做爲一個編碼單元。而且,它與標準的 ASCII 編碼是徹底兼容的。也就是說,在 [0x00, 0x7F] 的範圍內,這兩種編碼表示的字符都是相同的。這也是 UTF-8 編碼格式的一個巨大優點。
UTF-8 是一種可變寬的編碼方案。換句話說,它會用一個或多個字節的二進制數來表示某個字符,最多使用四個字節。好比,對於一個英文字符,它僅用一個字節的二進制數就能夠表示,而對於一箇中文字符,它須要使用三個字節纔可以表示。不論怎樣,一個受支持的字符老是能夠由 UTF-8 編碼爲一個字節序列。如下會簡稱後者爲 UTF-8 編碼值。
在 Go 語言中,一個 string 類型的值既能夠被拆分爲一個包含多個字符
的序列,也能夠被拆分爲一個包含多個字節
的序列。
前者能夠由一個以 rune
爲元素類型的切片來表示,然後者則能夠由一個以 byte
爲元素類型的切片表明。
rune
是 Go 語言特有的一個基本數據類型,它的一個值就表明一個字符,即:一個 Unicode 字符
(再通俗點,就是一箇中文字符,佔 3byte)。
從 Golang 語言的源碼 (https://github.com/golang/go/blob/master/src/builtin/builtin.go#L92) 中咱們其實能夠知道,rune
類型底層實際上是一個 int32
類型。
咱們已經知道,UTF-8 編碼
方案會把一個 Unicode 字符
編碼爲一個長度在 [1, 4] 範圍內的字節序列
,也就是說,一個 rune 類型的值會由四個字節寬度的空間來存儲。它的存儲空間老是可以存下一個 UTF-8 編碼值。
咱們能夠看以下代碼:
func unicodeAndUtf8() {
tempStr := "BGBiao 的SRE人生." fmt.Printf("string:%q\n",tempStr) fmt.Printf("rune(char):%q\n",[]rune(tempStr)) fmt.Printf("rune(hex):%x\n",[]rune(tempStr)) fmt.Printf("bytes(hex):% x\n",[]byte(tempStr)) } 複製代碼
對應輸出的效果以下:
string:"BGBiao 的SRE人生."
rune(char):['B' 'G' 'B' 'i' 'a' 'o' ' ' '的' 'S' 'R' 'E' '人' '生' '.'] rune(hex):[42 47 42 69 61 6f 20 7684 53 52 45 4eba 751f 2e] bytes(hex):42 47 42 69 61 6f 20 e7 9a 84 53 52 45 e4 ba ba e7 94 9f 2e 複製代碼
第二行輸出能夠看到字符串在被轉換爲 []rune
類型的值時,其中每一個字符都會成爲一個獨立的 rune
類型的元素值。而每一個 rune
底層的值都是採用 UTF-8
編碼值來表達的,因此第三行的輸出,咱們採用 16 進制數來表示上述字符串,每個 16 進制的字符分別表示一個字符,咱們能夠看到,當遇到中文字符時,因爲底層存儲須要更大的空間,因此使用的 16 進制數字也比較大,好比 4eba
和 751f
分別表明人
和生
。
但其實,當咱們將整個字符的 UTF-8 編碼值都拆成響應的字節序列時,就變成了第四行的輸出,能夠看到一箇中文字符其實底層是佔用了三個 byte,好比 e4 ba ba
和 e7 94 9f
分別對應 UFT-8
編碼值的 4eba
和 751f
,也即中文字符中的人
和生
。
注意:
對於一個多字節的 UTF-8 編碼值來講,咱們能夠把它當作一個總體轉換爲單一的整數,也能夠先把它拆成字節序列,再把每一個字節分別轉換爲一個整數,從而獲得多個整數。
咱們對上述字符串的底層編碼進行圖形拆解:
總之,一個 string 類型的值會由若干個 Unicode 字符組成,每一個 Unicode 字符均可以由一個 rune 類型的值來承載。這些字符在底層都會被轉換爲 UTF-8 編碼值
,而這些 UTF-8 編碼值又會以字節序列
的形式表達和存儲
。
因此,一個 string 類型的值在底層就是一個可以表達若干個 UTF-8 編碼值的字節序列。
注意:
帶有 range 子句的 for 語句會先把被遍歷的字符串值拆成一個字節序列
,而後再試圖找出這個字節序列中包含的每個 UTF-8 編碼值,或者說每個 Unicode 字符。所以在 range for 語句中,賦給第二個變量的值是 UTF-8 編碼值表明的那個 Unicode 字符,其類型會是 rune。
咱們來看以下代碼:
func rangeString() {
tempStr := "BGBiao 人生"
for k,v := range tempStr {
fmt.Printf("%d : %q %x [% x]\n",k,v,[]rune(string(v)),[]byte(string(v)))
}
}
複製代碼
使用 for range 進行遍歷字符串,獲得以下結果:
0 : 'B' [42] [42]
1 : 'G' [47] [47]
2 : 'B' [42] [42]
3 : 'i' [69] [69]
4 : 'a' [61] [61]
5 : 'o' [6f] [6f]
6 : ' ' [20] [20]
7 : '人' [4eba] [e4 ba ba]
10 : '生' [751f] [e7 94 9f]
複製代碼
能夠看到,遍歷字符串中的每一個字符時,對應的表示方式和咱們上圖中分析的是一致的,可是你有沒有發現一個小問題呢?
即在遍歷過程當中,最後一個字符生
的索引一下從 7
變成了 10
,這是由於人
這個字符底層是由三個字節共同表達的,即 [e4 ba ba]
,所以下一個字符的索引值就須要加 3,而生
的索引值也就變成了 10
而不是 8。
因此,須要注意的是: for range 語句能夠逐一的迭代出字符串值裏的每一個 Unicode 字符,可是相鄰的 Unicode 字符的索引值並不必定是連續的,這取決於前一個 Unicode 字符是否爲單字節字符。
本文使用 mdnice 排版