Golang 中的 Unicode 與 UTF-8
大多數(shù)的我們,真正認(rèn)識(shí)到有字符編碼這回事,一般都是因?yàn)橛龅搅藖y碼,因?yàn)槲覈S玫木幋a是 GBK 以及 GB2312:用兩個(gè) Byte 來表示所有的漢字,這樣,我們一共可以表示 2^16 = 65536 個(gè)字符,一旦我們的 GBK 以及 GB2312 編碼遇到了其他編碼,比如日本,韓國的編碼,就會(huì)變成亂碼,當(dāng)然,這時(shí)候如果是 UTF-8,也會(huì)亂碼。
我們知道,在計(jì)算機(jī)內(nèi)部,為了把二進(jìn)制數(shù)據(jù)轉(zhuǎn)換為顯示器上,需要進(jìn)行編碼,即將可顯示的字符一一對(duì)應(yīng)到二進(jìn)制數(shù)據(jù)上,比如 ASCII 碼,就是用一個(gè) Byte 的數(shù)據(jù)來表示英文字符加上一些英文符號(hào)。
至于中文,我們顯然不能使用僅僅一個(gè) Byte 來表示,我們需要用到更大的空間。
Unicode 與 Code point
在如今這個(gè)小小的世界村里,有著那么多的語言與文字,為了兼容所有的字符,Unicode 出現(xiàn)了,但是它需要有更多的 Byte 來將這個(gè)世界上所有的字符收納進(jìn)去(這里面甚至包含了 Emoji )。
為了了解 Unicode,你需要了解 Code point 即所謂的碼點(diǎn),也就是用 4 個(gè) Byte 大小的數(shù)字來表示所有的字符。
至于 Unicode 本身,你可以認(rèn)為它就是 Code point 的集合,而 UTF-8 呢?就是 Unicode 的編碼方式。
Unicode 與 UTF-8 編碼
下面的圖來自 UTF-8 的截圖:

這幅圖簡單明了的告訴我們,UTF-8 的編碼方式,比如漢字一般用三個(gè) Byte,每個(gè) Byte 的開頭都是固定的,各種文字軟件解析 UTF-8 編碼的時(shí)候,它就會(huì)按照這個(gè)格式去解析,一旦解析錯(cuò)誤(畢竟還可能會(huì)有不符合要求的數(shù)據(jù),或者是文件錯(cuò)誤了),錯(cuò)誤的字節(jié)就會(huì)被替換為 “�” (U+FFFD),然后神奇的地方就來了: 即使遇到這種錯(cuò)誤,它也不會(huì)影響接下來的其他字符的解析 ,因?yàn)檫@種編碼不必從頭開始,使得它可以 自我同步(Self-synchronizing) 。與此同時(shí),其它的一些編碼一旦遇到錯(cuò)誤編碼就會(huì)出問題,導(dǎo)致錯(cuò)誤編碼之后的正確編碼也會(huì)跟著出錯(cuò)。
當(dāng)然,UTF-8 編碼也有缺點(diǎn),由于它是可變的,當(dāng)英文字符偏多的時(shí)候,它會(huì)省空間,然而比如當(dāng)中文偏多的時(shí)候,它理論上(3 Byte)會(huì)比 GBK 編碼(2 Byte)最多多出 1/3 的存儲(chǔ)空間。
UTF-8 的例子
我們拿 Unicode 中最受歡迎的 Emoji 表情 :joy: 1 來舉例:它的 Code point 是 U+1F602 (對(duì), 1F602 是以 16 進(jìn)制表示的),然而在內(nèi)存中它的存儲(chǔ)方式的卻是 0xf09f9882 ,為什么?這就是 UTF-8 的編碼了(注意對(duì)比上圖的編碼方式):
- 000 011111 011000 000010 1f602
- 11110000 10011111 10011000 10000010 f0 9f 98 82
通過把 UTF-8 的編碼格子里面數(shù)據(jù)提取出來,我們就能獲得 Code point 1F602 。
你也可以用 Golang 來查看其它字符的編碼:
- package main
- import ( "fmt"
- "unicode/utf8"
- )func main() {
- fmt.Printf("%b\n", []byte(`:joy:`))
- fmt.Printf("% x\n", []byte(`:joy:`))
- r, _ := utf8.DecodeRuneInString(`:joy:`)
- fmt.Printf("% b\n", r)
- fmt.Printf("% x\n", r)
- }
Unicode 的其他編碼
Unicode 當(dāng)然不止一種編碼,還有 UTF-16、UTF-32 等,它們的關(guān)系就是 UTF-16 用 2 個(gè) Byte 來表示 UTF-8 分別用 1/2/3 個(gè) Byte 來表示的字符,然后 4 個(gè) Byte 與 UTF-8 一致,UTF-32 是完全用 4 個(gè) Byte 來表示所有的字符,另外,詳細(xì)的可以在 Comparison of Unicode encodings 中看到,
好,基礎(chǔ)講完,現(xiàn)在開始正式介紹。
Unicode 與 Golang 2
這里特別需要提到的是 Golang 與 UTF-8 的關(guān)系,他們背后的男人,都是 Ken Thompson 跟 Rob Pike 3 4 5 ,由此,大家就會(huì)明白 Golang 的 UTF-8 設(shè)計(jì)是有多么重要的參考意義。比如 Golang 設(shè)計(jì)了一個(gè) rune 類型來取代 Code point 的意義。
rune 看源碼就知道,它就是 int32,剛好 4 個(gè) Byte,剛可以用來表示 Unicode 的所有編碼 UTF-8 與 UTF-16。
在繼續(xù)之前,我想幫各位明白一個(gè)事實(shí):Golang 的源碼是默認(rèn) UTF-8 編碼的,這點(diǎn)從上面我給出的例子中就能明白,所以表情字符在編譯的時(shí)候,就已經(jīng)能被解析。
好了,那么我們來看看 Golang 的 unicode 包,其中就會(huì)有很多有用的判斷函數(shù):
- func IsControl(r rune) bool
- func IsDigit(r rune) bool
- func IsGraphic(r rune) bool
- func IsLetter(r rune) bool
- func IsLower(r rune) bool
- func IsMark(r rune) bool
- func IsNumber(r rune) bool
- func IsPrint(r rune) bool
- func IsPunct(r rune) bool
- func IsSpace(r rune) bool
- func IsSymbol(r rune) bool
- func IsTitle(r rune) bool
- func IsUpper(r rune) bool
另外,在 src/unicode/tables.go 中,有大量的 Unicode 中,各類字符的 Code point 區(qū)間,會(huì)有比較大的參考價(jià)值。
再看看 unicode/utf8 包,這里面的函數(shù),大多數(shù)時(shí)候你都用不到,但是有這么幾類情況就需要你必須得用到了:
- 統(tǒng)計(jì)字符數(shù)量;
- 轉(zhuǎn)編碼,比如將 GBK 轉(zhuǎn)為 UTF-8;
- 判斷字符串是否是 UTF-8 編碼,或者是否含有不符合 UTF-8 編碼的字符;
后面兩個(gè)可以忽略,第一個(gè)需要特地提醒下:
- s := `:joy:`
- fmt.Println(len(s))
這句輸出是什么?上面提過了,剛好就是 4。于是,你不能使用 len 來獲取字符數(shù)量,也就不能以此來判斷用戶輸入的字符是不是超過了系統(tǒng)的限制。另外,你也不能通過 s[0] 這樣的方式來獲取字符,因?yàn)檫@樣你只能取到這 4 個(gè) Byte 中的第一個(gè),也就是 0xf0 。
你應(yīng)該做的就是把 string 轉(zhuǎn)為 rune 數(shù)組,然后再去進(jìn)行字符的操作。
具體的使用方法就不細(xì)談了,相信你們能搞定。
另外,這里需要另外提示下,在 Node.js 中,string 本身就是 Unicode,而不是像 Golang 的 string 是二進(jìn)制,因此在這里可以認(rèn)為 Node.js 的 Buffer 才是 Golang 中的 string。
好了,最后留給你一個(gè)思考題:在 Node.js 中,為什么在處理 Buffer 時(shí)候,不能直接拼接?