go 語言 interface{} 的易錯點


一,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 轉換成實例對象后再進行判空

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM