天天看點

Go語言核心36講(Go語言實戰與應用十四)--學習筆記

在開始今天的内容之前,我先來做一個簡單的總結。

在資料類型方面有:

基于底層數組的切片;

用來傳遞資料的通道;

作為一等類型的函數;

可實作面向對象的結構體;

能無侵入實作的接口等。

在文法方面有:

異步程式設計神器go語句;

函數的最後關卡defer語句;

可做類型判斷的switch語句;

多通道操作利器select語句;

非常有特色的異常處理函數panic和recover。

除了這些,我們還一起讨論了測試 Go 程式的主要方式。這涉及了 Go 語言自帶的程式測試套件,相關的概念和工具包括:

獨立的測試源碼檔案;

三種功用不同的測試函數;

專用的testing代碼包;

功能強大的go test指令。

另外,就在前不久,我還為你深入講解了 Go 語言提供的那些同步工具。它們也是 Go 語言并發程式設計工具箱中不可或缺的一部分。這包括了:

經典的互斥鎖;

讀寫鎖;

條件變量;

原子操作。

以及 Go 語言特有的一些資料類型,即:

單次執行小助手sync.Once;

臨時對象池sync.Pool;

幫助我們實作多 goroutine 協作流程的sync.WaitGroup、context.Context;

一種高效的并發安全字典sync.Map。

在後面的日子裡,我會與你一起去探究 Go 語言标準庫中最常用的那些代碼包,弄清它們的用法、了解它們的機理。當然了,我還會順便講一講那些必備的周邊知識。

首先,讓我們來關注字元編碼方面的問題。這應該是在計算機軟體領域中非常基礎的一個問題了。

我在前面說過,Go 語言中的辨別符可以包含“任何 Unicode 編碼可以表示的字母字元”。我還說過,雖然我們可以直接把一個整數值轉換為一個string類型的值。

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

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

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

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

在這裡,我們首先要對 Unicode 編碼規範有所了解。不過,在講述它之前,我先來簡要地介紹一下 ASCII 編碼。

ASCII 是英文“American Standard Code for Information Interchange”的縮寫,中文譯為美國資訊交換标準代碼。它是由美國國家标準學會(ANSI)制定的單位元組字元編碼方案,可用于基于文本的資料交換。

它最初是美國的國家标準,後又被國際标準化組織(ISO)定為國際标準,稱為 ISO 646 标準,并适用于所有的拉丁文字字母。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 編碼值。

現在,在你初步地了解了這些知識之後,請認真地思考并回答下面的問題。别擔心,我會在後面進一步闡述 Unicode、UTF-8 以及 Go 語言對它們的運用。

問題:一個string類型的值在底層是怎樣被表達的?

典型回答 是在底層,一個string類型的值是由一系列相對應的 Unicode 代碼點的 UTF-8 編碼值來表達的。

在 Go 語言中,一個string類型的值既可以被拆分為一個包含多個字元的序列,也可以被拆分為一個包含多個位元組的序列。

前者可以由一個以rune為元素類型的切片來表示,而後者則可以由一個以byte為元素類型的切片代表。

rune是 Go 語言特有的一個基本資料類型,它的一個值就代表一個字元,即:一個 Unicode 字元。

比如,'G'、'o'、'愛'、'好'、'者'代表的就都是一個 Unicode 字元。

我們已經知道,UTF-8 編碼方案會把一個 Unicode 字元編碼為一個長度在[1, 4]範圍内的位元組序列。是以,一個rune類型的值也可以由一個或多個位元組來代表。

根據rune類型的聲明可知,它實際上就是int32類型的一個别名類型。也就是說,一個rune類型的值會由四個位元組寬度的空間來存儲。它的存儲空間總是能夠存下一個 UTF-8 編碼值。

一個rune類型的值在底層其實就是一個 UTF-8 編碼值。前者是(便于我們人類了解的)外部展現,後者是(便于計算機系統了解的)内在表達。

請看下面的代碼:

字元串值"Go愛好者"如果被轉換為[]rune類型的值的話,其中的每一個字元(不論是英文字元還是中文字元)就都會獨立成為一個rune類型的元素值。是以,這段代碼列印出的第二行内容就會如下所示:

又由于,每個rune類型的值在底層都是由一個 UTF-8 編碼值來表達的,是以我們可以換一種方式來展現這個字元序列:

可以看到,五個十六進制數與五個字元相對應。很明顯,前兩個十六進制數47和6f代表的整數都比較小,它們分别表示字元'G'和'o'。

因為它們都是英文字元,是以對應的 UTF-8 編碼值用一個位元組表達就足夠了。一個位元組的編碼值被轉換為整數之後,不會大到哪裡去。

而後三個十六進制數7231、597d和8005都相對較大,它們分别表示中文字元'愛'、'好'和'者'。

這些中文字元對應的 UTF-8 編碼值,都需要使用三個位元組來表達。是以,這三個數就是把對應的三個位元組的編碼值,轉換為整數後得到的結果。

我們還可以進一步地拆分,把每個字元的 UTF-8 編碼值都拆成相應的位元組序列。上述代碼中的第五行就是這麼做的。它會得到如下的輸出:

這裡得到的位元組切片比前面的字元切片明顯長了很多。這正是因為一個中文字元的 UTF-8 編碼值需要用三個位元組來表達。

這個位元組切片的前兩個元素值與字元切片的前兩個元素值是一緻的,而在這之後,前者的每三個元素值才對應字元切片中的一個元素值。

注意,對于一個多位元組的 UTF-8 編碼值來說,我們可以把它當做一個整體轉換為單一的整數,也可以先把它拆成位元組序列,再把每個位元組分别轉換為一個整數,進而得到多個整數。

這兩種表示法展現出來的内容往往會很不一樣。比如,對于中文字元'愛'來說,它的 UTF-8 編碼值可以展現為單一的整數7231,也可以展現為三個整數,即:e7、88和b1。

Go語言核心36講(Go語言實戰與應用十四)--學習筆記

(字元串值的底層表示)

總之,一個string類型的值會由若幹個 Unicode 字元組成,每個 Unicode 字元都可以由一個rune類型的值來承載。

這些字元在底層都會被轉換為 UTF-8 編碼值,而這些 UTF-8 編碼值又會以位元組序列的形式表達和存儲。是以,一個string類型的值在底層就是一個能夠表達若幹個 UTF-8 編碼值的位元組序列。

帶有range子句的for語句會先把被周遊的字元串值拆成一個位元組序列,然後再試圖找出這個位元組序列中包含的每一個 UTF-8 編碼值,或者說每一個 Unicode 字元。

這樣的for語句可以為兩個疊代變量指派。如果存在兩個疊代變量,那麼賦給第一個變量的值,就将會是目前位元組序列中的某個 UTF-8 編碼值的第一個位元組所對應的那個索引值。

而賦給第二個變量的值,則是這個 UTF-8 編碼值代表的那個 Unicode 字元,其類型會是rune。

例如,有這麼幾行代碼:

這裡被周遊的字元串值是"Go愛好者"。在每次疊代的時候,這段代碼都會列印出兩個疊代變量的值,以及第二個值的位元組序列形式。完整的列印内容如下:

第一行内容中的關鍵資訊有0、'G'和[47]。這是由于這個字元串值中的第一個 Unicode 字元是'G'。該字元是一個單位元組字元,并且由相應的位元組序列中的第一個位元組表達。這個位元組的十六進制表示為47。

第二行展示的内容與之類似,即:第二個 Unicode 字元是'o',由位元組序列中的第二個位元組表達,其十六進制表示為6f。

再往下看,第三行展示的是'愛',也是第三個 Unicode 字元。因為它是一個中文字元,是以由位元組序列中的第三、四、五個位元組共同表達,其十六進制表示也不再是單一的整數,而是e7、88和b1組成的序列。

下面要注意了,正是因為'愛'是由三個位元組共同表達的,是以第四個 Unicode 字元'好'對應的索引值并不是3,而是2加3後得到的5。

這裡的2代表的是'愛'對應的索引值,而3代表的則是'愛'對應的 UTF-8 編碼值的寬度。對于這個字元串值中的最後一個字元'者'來說也是類似的,是以,它對應的索引值是8。

由此可以看出,這樣的for語句可以逐一地疊代出字元串值裡的每個 Unicode 字元。但是,相鄰的 Unicode 字元的索引值并不一定是連續的。這取決于前一個 Unicode 字元是否為單位元組字元。

正因為如此,如果我們想得到其中某個 Unicode 字元對應的 UTF-8 編碼值的寬度,就可以用下一個字元的索引值減去目前字元的索引值。

初學者可能會對for語句的這種行為感到困惑,因為它給予兩個疊代變量的值看起來并不總是對應的。不過,一旦我們了解了它的内在機制就會撥雲見日、豁然開朗。

我們今天把目光聚焦在了 Unicode 編碼規範、UTF-8 編碼格式,以及 Go 語言對字元串和字元的相關處理方式上。

Go 語言的代碼是由 Unicode 字元組成的,它們都必須由 Unicode 編碼規範中的 UTF-8 編碼格式進行編碼并存儲,否則就會導緻 go 指令的報錯。

Unicode 編碼規範中的編碼格式定義的是:字元與位元組序列之間的轉換方式。其中的 UTF-8 是一種可變寬的編碼方案。

它會用一個或多個位元組的二進制數來表示某個字元,最多使用四個位元組。一個受支援的字元,總是可以由 UTF-8 編碼為一個位元組序列,後者也可以被稱為 UTF-8 編碼值。

Go 語言中的一個string類型值會由若幹個 Unicode 字元組成,每個 Unicode 字元都可以由一個rune類型的值來承載。

初學者可能會對帶有range子句的for語句周遊字元串值的行為感到困惑,因為它給予兩個疊代變量的值看起來并不總是對應的。但事實并非如此。

這樣的for語句會先把被周遊的字元串值拆成一個位元組序列,然後再試圖找出這個位元組序列中包含的每一個 UTF-8 編碼值,或者說每一個 Unicode 字元。

相鄰的 Unicode 字元的索引值并不一定是連續的。這取決于前一個 Unicode 字元是否為單位元組字元。一旦我們清楚了這些内在機制就不會再困惑了。

對于 Go 語言來說,Unicode 編碼規範和 UTF-8 編碼格式算是基礎之一了。我們應該了解到它們對 Go 語言的重要性。這對于正确了解 Go 語言中的相關資料類型以及日後的相關程式編寫都會很有好處。

今天的思考題是:判斷一個 Unicode 字元是否為單位元組字元通常有幾種方式?

https://github.com/MingsonZheng/go-core-demo

Go語言核心36講(Go語言實戰與應用十四)--學習筆記

本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協定進行許可。

歡迎轉載、使用、重新釋出,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用于商業目的,基于本文修改後的作品務必以相同的許可釋出。