仔細研究 Go(golang) 型別系統
通過示例詳解 Go 的型別系統
讓我們從一個非常基本的問題開始吧!
為什麼我們需要型別
在回答這個問題之前,我們需要先看看程式語言的一些原始抽象層,日常的工作我們並不需要處理這些層。
我們如何才能獲得資料的機器表示呢?
機器所能理解的是二進位制 0 和 1。但對我們來說,意義何在?直到看到這樣的東西,我才知道(有人是黑客帝國迷嗎?)
我們將這些二進位制抽象出來,並進一步考慮。
看下這段彙編程式碼
你能說出暫存器 R1、R2、R3 中的資料型別是什麼嗎 ?
你可能希望他們是整數,因為在組合語言層面也無法確定。沒有什麼能阻止 R1、R2、R3 具有任意型別,它們只是一堆 0 和 1 的暫存器。即使沒有意義,加法運算也會將 R2 和 R3 加在一起,產生一個位模式,並將結果儲存在 R1 中。
所以型別的概念從更高層次的抽象開始,在更高階的語言中,比如 C、Go、Java、Python、JavaScript 等等,這是語言本身的特性。
什麼是型別
型別的概念在不同的程式語言之間是不同的,可以用許多不同的方式來表達,但是大體上它們都有一些相同點。
- 型別是一組值;
- 在這些值上可以執行一組操作,例如:int 型別可以執行
+
和-
等運算,而對於字元型別,可以執行連線、空檢查等操作;
因此,語言型別系統指定哪些運算子對哪些型別有效。
型別檢查的目標是確保運算子使用在正確的型別上。通過執行型別檢查,可以強制執行預期的值解釋,因為一旦我們得到一堆只是 0 和 1 的機器碼,就執行不了任務檢查。機器也會很愉快地在這些機器碼上執行我們告訴它的任何操作。
型別系統用來強制執行位模式的預期解釋,確保整數的位模式不會有任何非整數操作,從而得到無意義的結果。
Go 的型別系統
有一些基本規範控制 Go 的型別系統,我們會看一些重要的。
但我不會一次把所有的概念都列出來,在這裡我將嘗試用不同的例子來涵蓋 Go 型別系統的一些基本概念,然後在講解一些基本概念的同時帶你瀏覽這些例子。
花點時間看看這些程式碼片段。其中哪一個會編譯,為什麼 ?
我希望你寫下你的答案和理由,這樣,我們最後就可以一起來驗證它。
命名型別
具有名稱的型別:例如 int、int64、float32、string、bool 等,這些都是預先宣告的。
另外,使用 type 關鍵字宣告的任意型別也稱為命名型別。
var i int // named type type myInt int // named type var b bool // named type
命名 ( 定義 ) 型別總是與任何其他型別不同。
未命名型別
複合型別,包括 array、struct、pointer、function、interface、slice、Map 和 channel,都是未命名型別。
[]string // unnamed type map[string]string // unnamed type [10]int // unnamed type
因為它們沒有名稱,但是有關於它們是如何組成的型別字面量描述符
底層型別
每種型別 T 都有底層型別
如果 T 是預定義型別的一種,包括 bool、數字、string 或者 字面量型別,則對應的底層型別是 T 本身;否則,T 的底層型別是 T 在其型別宣告中引用的型別的底層型別。
第 3、 8 行,預宣告的字串型別,因此底層型別是 T 本身,即字串;
第 5 、7 行,是字面量型別,因此底層型別就是 T 本身,即 map[string]int 和 指標 *N。注意:字面量型別也是未命名型別;
第 4 、6、10 行,T 的底層型別是 T 在其型別宣告中引用的型別的底層型別,例如:B 引用了 A,所以 B 的底層型別是字串型別,其他情況同理;
我們再來看下第 9 行的例子:type T map[S]int ,由於 S 的底層型別是 string,難道 type T map[S]int 的底層型別不應該是 map[string]int 而不是 map[S]int 嗎?因為我們在談論 map[S]int 的底層未命名型別,所以向下追溯到未命名型別(或者,正如 Go 語言規範上寫的一樣:如果 T 是型別字面量,則對應的底層型別就是 T 本身)。
你可能會想,為什麼我會如此重視未命名型別、命名型別和底層型別的規範。因為它們在 Go 語言規範中扮演重要的角色,我們將進一步討論,以幫助我們理解為什麼上面展示的程式碼片段有的能編譯而有的不能編譯,即使它們的意思基本相同。
可賦值性
當變數 a 可以賦值給型別 T 的變數時。
雖然這些條件已經在規範裡解釋過了,我們來看其中的一條規範,當分配時, 兩者都應該具有相同的底層型別,並且至少其中一個不是命名型別 。
我們來看下圖 4、 5 所示程式碼段的問題 :
package main type aInt int func main() { var i int = 10 var ai aInt = 100 i = ai printAiType(i) } func printAiType(ai aInt) { print(ai) }
上面的程式碼編譯不通過,編譯時報錯:
8:4: cannot use ai (type aInt) as type int in assignment 9:13: cannot use i (type int) as type aInt in argument to printAiType
因為 i 是命名型別 int,而 ai 是命名型別 aInt,雖然它們的底層型別相同。
package main type MyMap map[int]int func main() { m := make(map[int]int) var mMap MyMap mMap = m printMyMapType(mMap) print(m) } func printMyMapType(mMap MyMap) { print(mMap) }
上面這段程式碼編譯通過,因為 m 是未命名型別並且 m 和 mMap 的底層型別相同。
型別轉化
看下圖 3 的程式碼:
package main type Meter int64 type Centimeter int32 func main() { var cm Centimeter = 1000 var m Meter m = Meter(cm) print(m) cm = Centimeter(m) print(cm) }
上面的程式碼可以編譯通過,因為 Meter 和 Centimeter 都是整型,並且它們的底層型別可以相互轉化。
在看圖 1 和圖 2 的程式碼之前,我們來看看 Go 控制型別系統的另一個基本規範。
型別一致性
兩種型別要麼相同要麼不同。
已定義型別與其他任意型別總是不同。否則,如果兩種型別對應的底層型別在結構上是等效的,則它們是相同的。因此,即使預先宣告的命名型別 int、int64 等也是不相同的。
看下結構體的一條轉換規則:
不考慮結構體標籤,x 和 T 具有相同的底層型別(x 賦值給 T)。
package main type Meter struct { value int64 } type Centimeter struct { value int32 } func main() { cm := Centimeter{ value: 1000, } var m Meter m = Meter(cm) print(m.value) cm = Centimeter(m) print(cm.value) }
記住一點: 相同的底層型別 。由於成員 Meter.value 的底層型別是 int64,而成員 Centimeter.value 的底層型別是 int32,所以它們不相同,因為 已定義型別與其他任意型別總是不同 。所以圖 2 的程式碼片段編譯會出錯。
來看下圖 1 的程式碼段:
package main type Meter struct { value int64 } type Centimeter struct { value int64 } func main() { cm := Centimeter{ value: 1000, } var m Meter m = Meter(cm) print(m.value) cm = Centimeter(m) print(cm.value) } view raw
成員 Meter.value 和 Centimeter.value 的底層型別都是 int64,所以它們相同,編譯可以通過。
希望這篇文章對你理解 Go 型別系統有所幫助,這也是我一直寫文章的目的。