如何優雅的使用Go介面?
面向物件程式設計(OOP)中三個基本特徵分別是封裝,繼承,多型。在 Go 語言中封裝和繼承是通過 struct 來實現的,而多型則是通過介面(interface)來實現的。
什麼是介面
在 Go 語言中介面包含兩種含義:它既是方法的集合, 同時還是一種型別. 在Go 語言中是 隱式實現 的,意思就是對於一個具體的型別,不需要宣告它實現了哪些介面,只需要提供介面所必需的方法。
在 Go 語言的型別系統中有一個核心概念: 我們不應該根據型別可以容納哪種資料而是應該根據型別可以執行哪種操作來設計抽象型別 .
定義並實現介面
//宣告一個介面 type Human interface{ Say() } //定義兩個類,這兩個類分別實現了 Human 介面的 Say 方法 type women struct { } type man struct { } func (w *women) Say() { fmt.Println("I'm a women") } func(m *man) Say() { fmt.Println("I'm a man") } func main() { w := new(women) w.Say() m := new(man) m.Say() } //output //I'm a women //I'm a man
如果一個具體型別實現了某個介面的所有方法, 我們則成為該具體型別實現了該介面.注意: 必須是所有方法
介面型別
介面型別, 說白了就是空介面對於初學者來說很容易發生誤解, 對於空介面來說, 任何具體型別都實現了空介面. 舉個例子:
func Say(s interface{}) { // ... }
思考一下, 在 Say
函式內部, s 屬於什麼型別? 對於初學者來說很容易認為 s 屬於任意型別, 其實 s 屬於介面型別, 並不是任意型別, 但卻可以轉換成任意型別.
為什麼呢? 因為當我們往 Say
方法傳入值的時候, Go runtime 會自動的進行型別轉換, 將該值轉換成介面型別的值. 所有的值在執行時都 只會有一個型別, s 的靜態型別就是介面型別, 即 interface{}
對於像 Go 這種靜態型別的語言, 型別只是編譯時候的概念. 那 Go 是如何實現介面值動態轉換成任意型別值的呢?
在 Go 語言中, 介面值有兩部分組成, 一個指向該介面的具體型別的指標和另外一個指向該具體型別真實資料的指標. (檢視 interface在runtime2.go定義可以獲得)
type iface struct { tab*itab data unsafe.Pointer } type eface struct { _type *_type dataunsafe.Pointer }
明白資料儲存結構, 我們可以避免一些坑.例如下面的程式碼是有錯誤的:
package main import ( "fmt" ) func PrintAll(vals []interface{}) { for _, val := range vals { fmt.Println(val) } } func main() { names := []string{"stanley", "david", "oscar"} PrintAll(names) }
編譯會報錯: cannot use names (type []string) as type []interface {} in argument to PrintAll
因為 PrintAll
的入參是一個介面型別, 我們不能把 string
型別的值直接傳入. 再傳入之前需要進行轉換, 或者 PrintAll
內部函式實現進行型別斷言(後面會講到). 正確的程式碼:
func main() { names := []string{"stanley", "david", "oscar"} vals := make([]interface{}, len(names)) for i, v := range names { vals[i] = v } PrintAll(vals) }
指標或值接收者的區別
我們都知道, 在 Go 語言中所有的資料都是值傳遞. 實現介面方法如果全部使用值接收者或者全部使用指標接收者, 都很好理解. 那如果實現的方法既存在值接收者, 又存在指標接收者呢? 這個地方有陷阱, 我們通過例子來說明:
package main import "fmt" type Human interface { Say() } type Man struct { } type Woman struct { } func (m Man) Say() { fmt.Println("I'm a man") } func (w *Woman) Say() { fmt.Println("I'm a woman") } func main() { humans := []Human{Man{}, Woman{}} for _, human := range humans { human.Say() } }
上面程式碼會報錯: cannot use Woman literal (type Woman) as type Human in array or slice literal: Woman does not implement Human (Say method has pointer receiver) 提示 Woman 沒有實現 Human 介面, 這是因為 Woman 實現 Human 介面定義的是指標接收者, 但我們在 main
方法中傳入的是一個 Woman 的結構體轉為 Human 的介面值, 並不是一個指標, 因此報錯了. 如果我們將 main
函式略微改變一下:
func main() { humans := []Human{&Man{}, &Woman{}} for _, human := range humans { human.Say() } }
注意到在 main
方法中分別傳入了 Man 和 Woman 的指標, 但是編譯照樣通過了. 為什麼呢? Man實現 Human 介面定義的是值接收者, 並不是指標接收者. 原因就是在 Go 語言中所有的都是值傳遞, 儘管傳入的是 Man 的指標, 但是通過該指標我們可以找到其對應的值, Go 語言隱式幫我們做了型別轉換.我們記住在 Go 語言中指標型別可以獲得其關聯的任意值型別, 但反過來卻不行. 其實簡單的想一下, 一個具體值可能有無數個指標指向它, 但一個指標只會指向一個具體的值.
型別斷言
型別斷言是作用在介面值上的操作, 型別斷言的寫法如下:
<目標型別>, <布林引數> := <表示式>.(目標型別) //這種是安全的型別斷言, 不會引發 panic. <目標型別> := <表示式>.(目標型別) //這種是非安全的型別斷言, 如果斷言失敗會引發 panic.
我們看一個例子:
package main import "fmt" type Shape interface { Area() float64 } type Object interface { Volume() float64 } type Skin interface { Color() float64 } type Cube struct { side float64 } func (c Cube)Area() float64 { return c.side * c.side } func (c Cube)Volume() float64 { return c.side * c.side * c.side } func main() { var s Shape = Cube{3.0} value1, ok1 := s.(Object) fmt.Printf("dynamic value of Shape 's' with value %v implements interface Object? %v\n", value1, ok1) value2, ok2 := s.(Skin) fmt.Printf("dynamic value of Shape 's' with value %v implements interface Skin? %v\n", value2, ok2) }
因為在程式執行中, 有時會無法確定介面值的動態型別, 因此通過型別斷言可以來檢測其是否是一個特定的型別, 這樣便可以針對性的進行業務處理.
結合型別斷言, 我們就可以處理空介面的問題.比如說, 某個方法定義的入參型別為一個介面型別, 我們就可以在函式內部使用型別斷言處理不同的業務.
Go 語言中 Println 的實現就是通過型別斷言來處理的, 我們看一下原始碼的處理:
func Println(a ...interface{}) (n int, err error) { return Fprintln(os.Stdout, a...) } func Fprintln(w io.Writer, a ...interface{}) (n int, err error) { p := newPrinter() p.doPrintln(a) n, err = w.Write(p.buf) p.free() return } func (p *pp) doPrintln(a []interface{}) { for argNum, arg := range a { if argNum > 0 { p.buf.WriteByte(' ') } p.printArg(arg, 'v') } p.buf.WriteByte('\n') } func (p *pp) printArg(arg interface{}, verb rune) { //此處省略部分程式碼 //可以看到, 進行型別斷言來判斷需要輸出的內容. switch f := arg.(type) { case bool: p.fmtBool(f, verb) case float32: p.fmtFloat(float64(f), 32, verb) case float64: p.fmtFloat(f, 64, verb) case complex64: p.fmtComplex(complex128(f), 64, verb) case complex128: p.fmtComplex(f, 128, verb) case int: p.fmtInteger(uint64(f), signed, verb) case int8: p.fmtInteger(uint64(f), signed, verb) case int16: p.fmtInteger(uint64(f), signed, verb) case int32: p.fmtInteger(uint64(f), signed, verb) case int64: p.fmtInteger(uint64(f), signed, verb) case uint: p.fmtInteger(uint64(f), unsigned, verb) case uint8: p.fmtInteger(uint64(f), unsigned, verb) case uint16: p.fmtInteger(uint64(f), unsigned, verb) case uint32: p.fmtInteger(uint64(f), unsigned, verb) case uint64: p.fmtInteger(f, unsigned, verb) //篇幅原因, 僅顯示部分程式碼 }
總結
- 儘量考慮資料型別之間的相同功能來抽象介面, 而不是根據相同的欄位
- interface{}是一個介面型別, 不是任意型別
- 介面的資料結構分兩部分, 一部分指向其所表示的型別, 另一部分指向其具體型別的值
- 指標型別可以呼叫其指向的值的方法, 但是反過來處理不行
- Go 語言中所有的都是值傳遞
- 使用安全的型別斷言來判斷介面所代表的動態型別, 通過型別匹配可以幫助我們寫出更優雅通用並且安全的程式程式碼
Period.
更多文章歡迎掃碼關注公眾號:程式設計師 Morgan.