Go語言核心36講(Go語言基礎知識六)--學習筆記


06 | 程序實體的那些事兒 (下)

在上一篇文章,我們一直都在圍繞着可重名變量,也就是不同代碼塊中的重名變量,進行了討論。還記得嗎?

最后我強調,如果可重名變量的類型不同,那么就需要引起我們的特別關注了,它們之間可能會存在“屏蔽”的現象。

必要時,我們需要嚴格地檢查它們的類型,但是怎樣檢查呢?咱們現在就說。

我今天的問題是:怎樣判斷一個變量的類型?

我們依然以在上一篇文章中展示過的 demo11.go 為基礎。

package main

import "fmt"

var container = []string{"zero", "one", "two"}

func main() {
  container := map[int]string{0: "zero", 1: "one", 2: "two"}
  fmt.Printf("The element is %q.\n", container[1])
}

那么,怎樣在打印其中元素之前,正確判斷變量container的類型?

典型回答

答案是使用“類型斷言”表達式。具體怎么寫呢?

value, ok := interface{}(container).([]string)

這里有一條賦值語句。在賦值符號的右邊,是一個類型斷言表達式。

它包括了用來把container變量的值轉換為空接口值的interface{}(container)。

以及一個用於判斷前者的類型是否為切片類型 []string 的 .([]string)。

這個表達式的結果可以被賦給兩個變量,在這里由value和ok代表。變量ok是布爾(bool)類型的,它將代表類型判斷的結果,true或false。

如果是true,那么被判斷的值將會被自動轉換為[]string類型的值,並賦給變量value,否則value將被賦予nil(即“空”)。

順便提一下,這里的ok也可以沒有。也就是說,類型斷言表達式的結果,可以只被賦給一個變量,在這里是value。

但是這樣的話,當判斷為否時就會引發異常。

這種異常在 Go 語言中被叫做panic,我把它翻譯為運行時恐慌。因為它是一種在 Go 程序運行期間才會被拋出的異常,而“恐慌”二字是英文 Panic 的中文直譯。

除非顯式地“恢復”這種“恐慌”,否則它會使 Go 程序崩潰並停止。所以,在一般情況下,我們還是應該使用帶ok變量的寫法。

問題解析

正式說明一下,類型斷言表達式的語法形式是x.(T)。其中的x代表要被判斷類型的值。這個值當下的類型必須是接口類型的,不過具體是哪個接口類型其實是無所謂的。

所以,當這里的container變量類型不是任何的接口類型時,我們就需要先把它轉成某個接口類型的值。

如果container是某個接口類型的,那么這個類型斷言表達式就可以是container.([]string)。這樣看是不是清晰一些了?

在 Go 語言中,interface{}代表空接口,任何類型都是它的實現類型。

這里的具體語法是interface{}(x),例如前面展示的interface{}(container)。

你可能會對這里的{}產生疑惑,為什么在關鍵字interface的右邊還要加上這個東西?

請記住,一對不包裹任何東西的花括號,除了可以代表空的代碼塊之外,還可以用於表示不包含任何內容的數據結構(或者說數據類型)。

比如你今后肯定會遇到的struct{},它就代表了不包含任何字段和方法的、空的結構體類型。

而空接口interface{}則代表了不包含任何方法定義的、空的接口類型。

當然了,對於一些集合類的數據類型來說,{}還可以用來表示其值不包含任何元素,比如空的切片值[]string{},以及空的字典值map[int]string{}。

image

我們再向答案的最右邊看。圓括號中[]string是一個類型字面量。所謂類型字面量,就是用來表示數據類型本身的若干個字符。

比如,string是表示字符串類型的字面量,uint8是表示 8 位無符號整數類型的字面量。

再復雜一些的就是我們剛才提到的[]string,用來表示元素類型為string的切片類型,以及map[int]string,用來表示鍵類型為int、值類型為string的字典類型。

還有更復雜的結構體類型字面量、接口類型字面量,等等。

針對當前的這個問題,我寫了 demo12.go。它是 demo11.go 的修改版。我在其中分別使用了兩種方式來實施類型斷言,一種用的是我上面講到的方式,另一種用的是我們還沒討論過的switch語句,先供你參考。

package main

import (
	"fmt"
)

var container = []string{"zero", "one", "two"}

func main() {
	container := map[int]string{0: "zero", 1: "one", 2: "two"}

	// 方式1。
	_, ok1 := interface{}(container).([]string)
	_, ok2 := interface{}(container).(map[int]string)
	if !(ok1 || ok2) {
		fmt.Printf("Error: unsupported container type: %T\n", container)
		return
	}
	fmt.Printf("The element is %q. (container type: %T)\n",
		container[1], container)

	// 方式2。
	elem, err := getElement(container)
	if err != nil {
		fmt.Printf("Error: %s\n", err)
		return
	}
	fmt.Printf("The element is %q. (container type: %T)\n",
		elem, container)
}

func getElement(containerI interface{}) (elem string, err error) {
	switch t := containerI.(type) {
	case []string:
		elem = t[1]
	case map[int]string:
		elem = t[1]
	default:
		err = fmt.Errorf("unsupported container type: %T", containerI)
		return
	}
	return
}

可以看到,當前問題的答案可以只有一行代碼。你可能會想,這一行代碼解釋起來也太復雜了吧?

千萬不要為此煩惱,這其中很大一部分都是一些基本語法和概念,你只要記住它們就好了。但這也正是我要告訴你的,一小段代碼可以隱藏很多細節。面試官可以由此延伸到幾個方向繼續提問。這有點兒像潑墨,可以迅速由點及面。

知識擴展

問題 1. 你認為類型轉換規則中有哪些值得注意的地方?

類型轉換表達式的基本寫法我已經在前面展示過了。它的語法形式是T(x)。

其中的x可以是一個變量,也可以是一個代表值的字面量(比如1.23和struct{}{}),還可以是一個表達式。

注意,如果是表達式,那么該表達式的結果只能是一個值,而不能是多個值。在這個上下文中,x可以被叫做源值,它的類型就是源類型,而那個T代表的類型就是目標類型。

如果從源類型到目標類型的轉換是不合法的,那么就會引發一個編譯錯誤。那怎樣才算合法?具體的規則可參見 Go 語言規范中的轉換 https://golang.google.cn/ref/spec#Conversions 部分。

我們在這里要關心的,並不是那些 Go 語言編譯器可以檢測出的問題。恰恰相反,那些在編程語言層面很難檢測的東西才是我們應該關注的。

很多初學者所說的陷阱(或者說坑),大都源於他們需要了解但卻不了解的那些知識和技巧。因此,在這些規則中,我想拋出三個我認為很常用並且非常值得注意的知識點,提前幫你標出一些“陷阱”。

首先,對於整數類型值、整數常量之間的類型轉換,原則上只要源值在目標類型的可表示范圍內就是合法的。

比如,之所以uint8(255)可以把無類型的常量255轉換為uint8類型的值,是因為255在[0, 255]的范圍內。

但需要特別注意的是,源整數類型的可表示范圍較大,而目標類型的可表示范圍較小的情況,比如把值的類型從int16轉換為int8。請看下面這段代碼:

var srcInt = int16(-255)
dstInt := int8(srcInt)

變量srcInt的值是int16類型的-255,而變量dstInt的值是由前者轉換而來的,類型是int8。int16類型的可表示范圍可比int8類型大了不少。問題是,dstInt的值是多少?

首先你要知道,整數在 Go 語言以及計算機中都是以補碼的形式存儲的。這主要是為了簡化計算機對整數的運算過程。(負數的)補碼其實就是原碼各位求反再加 1。

比如,int16類型的值-255的補碼是1111111100000001。如果我們把該值轉換為int8類型的值,那么 Go 語言會把在較高位置(或者說最左邊位置)上的 8 位二進制數直接截掉,從而得到00000001。

又由於其最左邊一位是0,表示它是個正整數,以及正整數的補碼就等於其原碼,所以dstInt的值就是1。

一定要記住,當整數值的類型的有效范圍由寬變窄時,只需在補碼形式下截掉一定數量的高位二進制數即可。

類似的快刀斬亂麻規則還有:當把一個浮點數類型的值轉換為整數類型值時,前者的小數部分會被全部截掉。

第二,雖然直接把一個整數值轉換為一個string類型的值是可行的,但值得關注的是,被轉換的整數值應該可以代表一個有效的 Unicode 代碼點,否則轉換的結果將會是"�"(僅由高亮的問號組成的字符串值)。

字符'�'的 Unicode 代碼點是U+FFFD。它是 Unicode 標准中定義的 Replacement Character,專用於替換那些未知的、不被認可的以及無法展示的字符。

我肯定不會去問“哪個整數值轉換后會得到哪個字符串”,這太變態了!但是我會寫下:

string(-1)

並詢問會得到什么?這可是完全不同的問題啊。由於-1肯定無法代表一個有效的 Unicode 代碼點,所以得到的總會是"�"。在實際工作中,我們在排查問題時可能會遇到�,你需要知道這可能是由於什么引起的。

第三個知識點是關於string類型與各種切片類型之間的互轉的。

你先要理解的是,一個值在從string類型向[]byte類型轉換時代表着以 UTF-8 編碼的字符串會被拆分成零散、獨立的字節。

除了與 ASCII 編碼兼容的那部分字符集,以 UTF-8 編碼的某個單一字節是無法代表一個字符的。

string([]byte{'\xe4', '\xbd', '\xa0', '\xe5', '\xa5', '\xbd'}) // 你好

比如,UTF-8 編碼的三個字節\xe4、\xbd和\xa0合在一起才能代表字符'你',而\xe5、\xa5和\xbd合在一起才能代表字符'好'。

其次,一個值在從string類型向[]rune類型轉換時代表着字符串會被拆分成一個個 Unicode 字符。

string([]rune{'\u4F60', '\u597D'}) // 你好

當你真正理解了 Unicode 標准及其字符集和編碼方案之后,上面這些內容就會顯得很容易了。什么是 Unicode 標准?我會首先推薦你去它的官方網站 https://home.unicode.org/ 一探究竟。

問題 2. 什么是別名類型?什么是潛在類型?

我們可以用關鍵字type聲明自定義的各種類型。當然了,這些類型必須在 Go 語言基本類型和高級類型的范疇之內。在它們當中,有一種被叫做“別名類型”的類型。我們可以像下面這樣聲明它:

type MyString = string

這條聲明語句表示,MyString是string類型的別名類型。顧名思義,別名類型與其源類型的區別恐怕只是在名稱上,它們是完全相同的。

源類型與別名類型是一對概念,是兩個對立的稱呼。別名類型主要是為了代碼重構而存在的。更詳細的信息可參見 Go 語言官方的文檔Proposal: Type Aliases https://go.googlesource.com/proposal/+/master/design/18130-type-alias.md

Go 語言內建的基本類型中就存在兩個別名類型。byte是uint8的別名類型,而rune是int32的別名類型。

一定要注意,如果我這樣聲明:

type MyString2 string // 注意,這里沒有等號。

MyString2和string就是兩個不同的類型了。這里的MyString2是一個新的類型,不同於其他任何類型。

這種方式也可以被叫做對類型的再定義。我們剛剛把string類型再定義成了另外一個類型MyString2。

image

對於這里的類型再定義來說,string可以被稱為MyString2的潛在類型。潛在類型的含義是,某個類型在本質上是哪個類型。

潛在類型相同的不同類型的值之間是可以進行類型轉換的。因此,MyString2類型的值與string類型的值可以使用類型轉換表達式進行互轉。

但對於集合類的類型[]MyString2與[]string來說這樣做卻是不合法的,因為[]MyString2與[]string的潛在類型不同,分別是[]MyString2和[]string。另外,即使兩個不同類型的潛在類型相同,它們的值之間也不能進行判等或比較,它們的變量之間也不能賦值。

總結

Go 語言中的每個變量都是有類型的,我們可以使用類型斷言表達式判斷變量是哪個類型的。

正確使用該表達式需要一些小技巧,比如總是應該把結果賦給兩個變量。另外還要保證被判斷的變量是接口類型的,這可能會用到類型轉換表達式。

此外,你還應該搞清楚別名類型聲明與類型再定義之間的區別,以及由此帶來的它們的值在類型轉換、判等、比較和賦值操作方面的不同。

思考題

  • 除了上述提及的那些,你還認為類型轉換規則中有哪些值得注意的地方?
  • 你能具體說說別名類型在代碼重構過程中可以起到哪些作用嗎?

知識共享許可協議

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM