Go 語言型別系統詳解

Seekload發表於2019-03-18

這是『就要學習 Go 語言』系列的第 18 篇分享文章

什麼是型別

不同的程式語言之間,型別的概念有所不同,可以用許多不同的方式來表達,但大體上都有一些相同的地方。

  1. 型別是一組值;
  2. 相同型別的值之間可以進行哪些操作,例如:int 型別可以執行 + 和 - 等運算,而對於字元型別,可以執行連線、空檢查等操作;

因此,語言型別系統指定哪些運算子對哪些型別有效。

Go 語言的型別系統

boolean、numeric 和 string 是 Go 的基礎資料型別,也稱為預宣告型別(pre-declared type),可用來構造其他的型別,例如字面量型別。

字面量型別 (type literal):由預宣告型別組合而成(沒有用 type 關鍵字定義),例如:[3]int 、chan int、map[string] string、* int 等。

由字面量型別可構成複合型別,如:array、struct、map、slice、channel、func、interface 等。

命名型別和未命名型別

具有名稱的型別:例如 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 本身;否則,T 的底層型別是 T 在定義時引用的型別的底層型別。

type A string      // string
type B A			// string
type M map[string]int // map[string]int
type N M			// map[string]int
type P *N			// *N
type S string		// string
type T map[S]int	// map[S]int
type U T 			// map[S]int
複製程式碼

第 1、 6 行,預宣告的字串型別,因此底層型別是 T 本身,即字串;

第 3 、5 行,是字面量型別,因此底層型別就是 T 本身,即 map[string]int 和 指標 *N。注意:字面量型別也是未命名型別;

第 2 、4、8 行,T 的底層型別是 T 在其定義時引用的型別的底層型別,例如:B 引用了 A,所以 B 的底層型別是字串型別,其他情況同理;

我們再來看下第 7 行的例子:type T map[S]int ,由於 S 的底層型別是 string,難道此時 T 的底層型別不應該是 map[string]int 而不是 map[S]int 嗎?因為我們在談論 map[S]int 的底層未命名型別,所以向下追溯到未命名型別,正如 Go 語言規範上寫的一樣:如果 T 是字面量型別,則對應的底層型別就是 T 本身。

可賦值性

關於變數的可賦值性在 Go 語言的文件中已經講得很清楚了,我們來看其中比較重要的一條:當變數 a 可以賦值給型別 T 的變數時,兩者都應該具有相同的底層型別,並且至少其中一個不是命名型別

看下程式碼

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,雖然它們的底層型別相同,都是 int。

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 的底層型別相同。

型別轉化

看下型別轉化的規範

型別轉化規範

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)
}
複製程式碼

上面的程式碼可以編譯通過,因為 MeterCentimeter 都是整型,並且它們的底層型別可以相互轉化。

型別一致性

兩種型別要麼相同要麼不同。

已定義型別與其他任意型別總是不同。因此,即使預先宣告的命名型別 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,所以它們不相同,因為已定義型別與其他任意型別總是不同。所以上面的程式碼片段編譯會出錯。

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)
}
複製程式碼

成員 Meter.value 和 Centimeter.value 的底層型別都是 int64,所以它們相同,編譯可以通過。

ps:我在 GCTT(Go 中國翻譯組) 翻譯的第一篇文章就是關於 Go 語言的型別系統的,今天整理處理給大家看下,希望這篇文章對你理解 Go 型別系統有所幫助!


(全文完)

原創文章,若需轉載請註明出處!
歡迎掃碼關注公眾號「Golang來啦」或者移步 seekload.net ,檢視更多精彩文章。

公眾號「Golang來啦」給你準備了一份神祕學習大禮包,後臺回覆【電子書】領取!

公眾號二維碼

相關文章