一,interface 介紹
如果說 goroutine 和 channel 是 go 語言並發的兩大基石,那 interface 就是 go 語言類型抽象的關鍵。在實際項目中,幾乎所有的數據結構最底層都是接口類型。說起 C++ 語言,我們立即能想到是三個名詞:封裝、繼承、多態。go 語言雖然沒有嚴格意義上的對象,但通過 interface,可以說是實現了多態性。(由以組合結構體實現了封裝、繼承的特性)
go 語言中支持將 method、struct、struct 中成員定義為 interface 類型,使用 struct 舉一個簡單的栗子
1 package main 2 3 type animal interface { 4 Move() 5 } 6 7 type bird struct{} 8 9 func (self *bird) Move() { 10 println("bird move") 11 } 12 13 type beast struct{} 14 15 func (self *beast) Move() { 16 println("beast move") 17 } 18 19 func animalMove(v animal) { 20 v.Move() 21 } 22 23 func main() { 24 var a *bird 25 var b *beast 26 animalMove(a) // bird move 27 animalMove(b) // beast move 28 }
使用 go 語言的 interface 特性,就能實現多態性,進行泛型編程。
二,interface 原理
如果沒有充分了解 interface 的本質,就直接使用,那最終肯定會踩到很深的坑,要用就先要了解,先來看看 interface 源碼
1 type eface struct { 2 _type *_type 3 data unsafe.Pointer 4 } 5 6 type _type struct { 7 size uintptr // type size 8 ptrdata uintptr // size of memory prefix holding all pointers 9 hash uint32 // hash of type; avoids computation in hash tables 10 tflag tflag // extra type information flags 11 align uint8 // alignment of variable with this type 12 fieldalign uint8 // alignment of struct field with this type 13 kind uint8 // enumeration for C 14 alg *typeAlg // algorithm table 15 gcdata *byte // garbage collection data 16 str nameOff // string form 17 ptrToThis typeOff // type for pointer to this type, may be zero 18 }
可以看到 interface 變量之所以可以接收任何類型變量,是因為其本質是一個對象,並記錄其類型和數據塊的指針。(其實 interface 的源碼還包含函數結構和內存分布,由於不是本文重點,有興趣的同學可以自行了解)
三,interface 判空的坑
對於一個空對象,我們往往通過 if v == nil 的條件語句判斷其是否為空,但在代碼中充斥着 interface 類型的情況下,很多時候判空都並不是我們想要的結果(其實了解或聰明的同學從上述 interface 的本質是對象已經知道我想要說的是什么)
1 package main 2 3 type animal interface { 4 Move() 5 } 6 7 type bird struct{} 8 9 func (self *bird) Move() { 10 println("bird move") 11 } 12 13 type beast struct{} 14 15 func (self *beast) Move() { 16 println("beast move") 17 } 18 19 func animalMove(v animal) { 20 if v == nil { 21 println("nil animal") 22 } 23 v.Move() 24 } 25 26 func main() { 27 var a *bird // nil 28 var b *beast // nil 29 animalMove(a) // bird move 30 animalMove(b) // beast move 31 }
還是剛才的栗子,其實在 go 語言中 var a *bird 這種寫法,a 只是聲明了其類型,但並沒有申請一塊空間,所以這時候 a 本質還是指向空指針,但我們在 aminalMove 函數進行判空是失敗的,並且下面的 v.Move() 的調用也是成功的,本質的原因就是因為 interface 是一個對象,在進行函數調用的時候,就會將 bird 類型的空指針進行隱式轉換,轉換成實例的 interface animal 對象,所以這時候 v 其實並不是空,而是其 data 變量指向了空。這時候看着執行都正常,那什么情況下坑才會絆倒我們呢?只需要加一段代碼
1 package main 2 3 type animal interface { 4 Move() 5 } 6 7 type bird struct { 8 name string 9 } 10 11 func (self *bird) Move() { 12 println("bird move %s", self.name) // panic 13 } 14 15 type beast struct { 16 name string 17 } 18 19 func (self *beast) Move() { 20 println("beast move %s", self.name) // panic 21 } 22 23 func animalMove(v animal) { 24 if v == nil { 25 println("nil animal") 26 } 27 v.Move() 28 } 29 30 func main() { 31 var a *bird // nil 32 var b *beast // nil 33 animalMove(a) // panic 34 animalMove(b) // panic 35 }
在代碼中,我們給派生類添加 name 變量,並在函數的實現中進行調用,就會發生 panic,這時候的 self 其實是 nil 指針。所以這里坑就出來了。有些人覺得這類錯誤謹慎一些還是可以避免的,那是因為我們是正向思維去代入接口,但如果反向編程就容易造成很難發現的 bug
1 package main 2 3 type animal interface { 4 Move() 5 } 6 7 type bird struct { 8 name string 9 } 10 11 func (self *bird) Move() { 12 println("bird move %s", self.name) 13 } 14 15 type beast struct { 16 name string 17 } 18 19 func (self *beast) Move() { 20 println("beast move %s", self.name) 21 } 22 23 func animalMove(v animal) { 24 if v == nil { 25 println("nil animal") 26 } 27 v.Move() 28 } 29 30 func getBirdAnimal(name string) *bird { 31 if name != "" { 32 return &bird{name: name} 33 } 34 return nil 35 } 36 37 func main() { 38 var a animal 39 var b animal 40 a = getBirdAnimal("big bird") 41 b = getBirdAnimal("") // return interface{data:nil} 42 animalMove(a) // bird move big bird 43 animalMove(b) // panic 44 }
這里我們看到通過函數返回實例類型指針,當返回 nil 時,因為接收的變量為接口類型,所以進行了隱性轉換再次導致了 panic(這類反向轉換很難發現)。
那我們如何處理上述這類問題呢。我這邊整理了三個點
1,充分了解 interface 原理,使用過程中需要謹慎小心
2,謹慎使用泛型編程,接收變量使用接口類型,也需要保證接口返回為接口類型,而不應該是實例類型
3,判空是使用反射 typeOf 和 valueOf 轉換成實例對象后再進行判空