《快學 Go 語言》第 9 課 —— 介面

碼洞發表於2018-11-27

介面是一個物件的對外能力的展現,我們使用一個物件時,往往不需要知道一個物件的內部複雜實現,通過它暴露出來的介面,就知道了這個物件具備哪些能力以及如何使用這個能力。

我們常說「佛有千面」,不同的人看到的佛並不一樣。一個複雜的複合物件常常也可以是一個多面手,它具備多種能力,在形式上實現了多種介面。「弱水三千,只取一瓢」,使用時我們根據不同的場合來挑選滿足需要的介面能力來使用這個物件即可。

Go 語言的介面型別非常特別,它的作用和 Java 語言的介面一樣,但是在形式上有很大的差別。Java 語言需要在類的定義上顯式實現了某些介面,才可以說這個類具備了介面定義的能力。但是 Go 語言的介面是隱式的,只要結構體上定義的方法在形式上(名稱、引數和返回值)和介面定義的一樣,那麼這個結構體就自動實現了這個介面,我們就可以使用這個介面變數來指向這個結構體物件。下面我們看個例子

package main

import "fmt"

// 可以聞
type Smellable interface {
  smell()
}

// 可以吃
type Eatable interface {
  eat()
}

// 蘋果既可能聞又能吃
type Apple struct {}

func (a Apple) smell() {
  fmt.Println("apple can smell")
}

func (a Apple) eat() {
  fmt.Println("apple can eat")
}

// 花只可以聞
type Flower struct {}

func (f Flower) smell() {
  fmt.Println("flower can smell")
}

func main() {
  var s1 Smellable
  var s2 Eatable
  var apple = Apple{}
  var flower = Flower{}
  s1 = apple
  s1.smell()
  s1 = flower
  s1.smell()
  s2 = apple
  s2.eat()
}

--------------------
apple can smell
flower can smell
apple can eat
複製程式碼

上面的程式碼定義了兩種介面,Apple 結構體同時實現了這兩個介面,而 Flower 結構體只實現了 Smellable 介面。我們並沒有使用類似於 Java 語言的 implements 關鍵字,結構體和介面就自動產生了關聯。

空介面

如果一個介面裡面沒有定義任何方法,那麼它就是空介面,任意結構體都隱式地實現了空介面。

Go 語言為了避免使用者重複定義很多空介面,它自己內建了一個,這個空介面的名字特別奇怪,叫 interface{} ,初學者會非常不習慣。之所以這個型別名帶上了大括號,那是在告訴使用者括號裡什麼也沒有。我始終認為這種名字很古怪,它讓程式碼看起來有點醜陋。

空介面裡面沒有方法,所以它也不具有任何能力,其作用相當於 Java 的 Object 型別,可以容納任意物件,它是一個萬能容器。比如一個字典的 key 是字串,但是希望 value 可以容納任意型別的物件,類似於 Java 語言的 Map<String,Object> 型別,這時候就可以使用空介面型別 interface{}。

package main

import "fmt"

func main() {
 // 連續兩個大括號,是不是看起來很彆扭
	var user = map[string]interface{}{
		"age": 30,
		"address": "Beijing Tongzhou",
		"married": true,
	}
	fmt.Println(user)
	// 型別轉換語法來了
 var age = user["age"].(int)
	var address = user["address"].(string)
	var married = user["married"].(bool)
	fmt.Println(age, address, married)
}

-------------
map[age:30 address:Beijing Tongzhou married:true]
30 Beijing Tongzhou true
複製程式碼

程式碼中 user 字典變數的型別是 map[string]interface{},從這個字典中直接讀取得到的 value 型別是 interface{},需要通過型別轉換才能得到期望的變數。

介面變數的本質

在使用介面時,我們要將介面看成一個特殊的容器,這個容器只能容納一個物件,只有實現了這個介面型別的物件才可以放進去。

《快學 Go 語言》第 9 課 —— 介面

介面變數作為變數來說它也是需要佔據記憶體空間的,通過翻閱 Go 語言的原始碼可以發現,介面變數也是由結構體來定義的,這個結構體包含兩個指標欄位,一個欄位指向被容納的物件記憶體,另一個欄位指向一個特殊的結構體 itab,這個特殊的結構體包含了介面的型別資訊和被容納物件的資料型別資訊。

// interface structure
type iface struct {
  tab *itab  // 型別指標
  data unsafe.Pointer  // 資料指標
}

type itab struct {
  inter *interfacetype // 介面型別資訊
  _type *_type // 資料型別資訊
  ...
}
複製程式碼

既然介面變數只包含兩個指標欄位,那麼它的記憶體佔用應該是 2 個機器字,下面我們來編寫程式碼驗證一下

package main

import "fmt"
import "unsafe"

func main() {
	var s interface{}
	fmt.Println(unsafe.Sizeof(s))
	var arr = [10]int {1,2,3,4,5,6,7,8,9,10}
	fmt.Println(unsafe.Sizeof(arr))
	s = arr
	fmt.Println(unsafe.Sizeof(s))
}

----------
16
80
16
複製程式碼

陣列的記憶體佔用是 10 個機器字,但是這絲毫不會影響到介面變數的記憶體佔用。

用介面來模擬多型

前面我們說到,介面是一種特殊的容器,它可以容納多種不同的物件,只要這些物件都同樣實現了介面定義的方法。如果我們將容納的物件替換成另一個物件,那不就可以完成上一節我們沒有完成的多型功能了麼?好,順著這個思路,下面我們就來模擬一下多型

package main

import "fmt"

type Fruitable interface {
	eat()
}

type Fruit struct {
	Name string  // 屬性變數
	Fruitable  // 匿名內嵌介面變數
}

func (f Fruit) want() {
	fmt.Printf("I like ")
	f.eat() // 外結構體會自動繼承匿名內嵌變數的方法
}

type Apple struct {}

func (a Apple) eat() {
	fmt.Println("eating apple")
}

type Banana struct {}

func (b Banana) eat() {
	fmt.Println("eating banana")
}

func main() {
	var f1 = Fruit{"Apple", Apple{}}
	var f2 = Fruit{"Banana", Banana{}}
	f1.want()
	f2.want()
}

---------
I like eating apple
I like eating banana
複製程式碼

使用這種方式模擬多型本質上是通過組合屬性變數(Name)和介面變數(Fruitable)來做到的,屬性變數是物件的資料,而介面變數是物件的功能,將它們組合到一塊就形成了一個完整的多型性的結構體。

介面的組合繼承

介面的定義也支援組合繼承,比如我們可以將兩個介面定義合併為一個介面如下

type Smellable interface {
  smell()
}

type Eatable interface {
  eat()
}

type Fruitable interface {
  Smellable
  Eatable
}
複製程式碼

這時 Fruitable 介面就自動包含了 smell() 和 eat() 兩個方法,它和下面的定義是等價的。

type Fruitable interface {
  smell()
  eat()
}
複製程式碼

介面變數的賦值

變數賦值本質上是一次記憶體淺拷貝,切片的賦值是拷貝了切片頭,字串的賦值是拷貝了字串的頭部,而陣列的賦值呢是直接拷貝整個陣列。介面變數的賦值會不會不一樣呢?接下來我們做一個實驗

package main

import "fmt"

type Rect struct {
	Width int
	Height int
}

func main() {
	var a interface {}
	var r = Rect{50, 50}
	a = r

	var rx = a.(Rect)
	r.Width = 100
	r.Height = 100
	fmt.Println(rx)
}

------
{50 50}
複製程式碼

從上面的輸出結果中可以推斷出結構體的記憶體發生了複製,這個複製可能是因為賦值(a = r)也可能是因為型別轉換(rx = a.(Rect)),也可能是兩者都進行了記憶體複製。那能不能判斷出究竟在介面變數賦值時有沒有發生記憶體複製呢?不好意思,就目前來說我們學到的知識點還辦不到。到後面的高階階段我們將會使用 unsafe 包來洞悉其中的更多細節。不過我可以提前告訴你們答案是什麼,那就是兩者都會發生資料記憶體的複製 —— 淺拷貝。

指向指標的介面變數

如果將上面的例子改成指標,將介面變數指向結構體指標,那結果就不一樣了

package main

import "fmt"

type Rect struct {
	Width int
	Height int
}

func main() {
	var a interface {}
	var r = Rect{50, 50}
	a = &r // 指向了結構體指標

	var rx = a.(*Rect) // 轉換成指標型別
	r.Width = 100
	r.Height = 100
	fmt.Println(rx)
}

-------
&{100 100}
複製程式碼

從輸出結果中可以看出指標變數 rx 指向的記憶體和變數 r 的記憶體是同一份。因為在型別轉換的過程中只發生了指標變數的記憶體複製,而指標變數指向的記憶體是共享的。

《快學 Go 語言》第 9 課 —— 介面

微信掃一掃上面的二維碼,關注「碼洞」閱讀《快學 Go 語言》更多章節

相關文章