1. 程式人生 > 程式設計 >Golang中的Unicode與字串示例詳解

Golang中的Unicode與字串示例詳解

背景:

在我們使用Golang進行開發過程中,總是繞不開對字元或字串的處理,而在Golang語言中,對字元和字串的處理方式可能和其他語言不太一樣,比如Python或Java類的語言,本篇文章分享一些Golang語言下的Unicode和字串編碼。

Go語言字元編碼

注意: 在Golang語言中的識別符號可以包含 " 任何Unicode編碼可以標識的字母字元 "。

被轉換的整數值應該可以代表一個有效的 Unicode 程式碼點,否則轉換的結果就將會是 "�",即:一個僅由高亮的問號組成的字串值。

另外,當一個 string 型別的值被轉換為 []rune 型別值的時候,其中的字串會被拆分成一個一個的 Unicode 字元。

顯然,Go 語言採用的字元編碼方案從屬於 Unicode 編碼規範。更確切地說,Go 語言的程式碼正是由 Unicode 字元組成的。Go 語言的所有原始碼,都必須按照 Unicode 編碼規範中的 UTF-8 編碼格式進行編碼。

換句話說,Go 語言的原始碼檔案必須使用 UTF-8 編碼格式進行儲存。如果原始碼檔案中出現了非 UTF-8 編碼的字元,那麼在構建、安裝以及執行的時候,go 命令就會報告錯誤 " illegal UTF-8 encoding "。

ASCII 編碼

ASCII 編碼方案使用單個位元組(byte)的二進位制數來編碼一個字元。標準的 ASCII 編碼用一個位元組的最高位元(bit)位作為奇偶校驗位,而擴充套件的 ASCII 編碼則將此位也用於表示字元。ASCII 編碼支援的可列印字元和控制字元的集合也被叫做 ASCII 編碼集。

我們所說的 Unicode 編碼規範,實際上是另一個更加通用的、針對書面字元和文字的字元編碼標準。它為世界上現存的所有自然語言中的每一個字元,都設定了一個唯一的二進位制編碼。

它定義了不同自然語言的文字資料在國際間交換的統一方式,併為全球化軟體建立了一個重要的基礎。

Unicode 編碼規範以 ASCII 編碼集為出發點,並突破了 ASCII 只能對拉丁字母進行編碼的限制。它不但提供了可以對世界上超過百萬的字元進行編碼的能力,還支援所有已知的轉義序列和控制程式碼。

我們都知道,在計算機系統的內部,抽象的字元會被編碼為整數。這些整數的範圍被稱為程式碼空間。在程式碼空間之內,每一個特定的整數都被稱為一個程式碼點。

一個受支援的抽象字元會被對映並分配給某個特定的程式碼點,反過來講,一個程式碼點總是可以被看成一個被編碼的字元。

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 編碼值。

string型別的底層儲存

在 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 編碼值來說,我們可以把它當做一個整體轉換為單一的整數,也可以先把它拆成位元組序列,再把每個位元組分別轉換為一個整數,從而得到多個整數。

我們對上述字串的底層編碼進行圖形拆解:

Golang中的Unicode與字串示例詳解

總之,一個 string 型別的值會由若干個 Unicode 字元組成,每個 Unicode 字元都可以由一個 rune 型別的值來承載。這些字元在底層都會被轉換為 UTF-8 編碼值,而這些 UTF-8 編碼值又會以位元組序列的形式表達和儲存。

所以,一個 string 型別的值在底層就是一個能夠表達若干個 UTF-8 編碼值的位元組序列。

range遍歷字串示例

注意: 帶有 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字元是否為單位元組字元。

總結

到此這篇關於Golang中Unicode與字串示例的文章就介紹到這了,更多相關Golang Unicode與字串內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!