Go「一個包含nil指針的介面不是nil介面」踩坑

  • 2019 年 10 月 3 日
  • 筆記

最近在項目中踩了一個深坑——「Golang中一個包含nil指針的介面不是nil介面」,現象是函數內返回了nil給一個對象,使用interface接收函數返回值判斷始終不為nil。總結下分享出來,如果你不是很理解這句話,那推薦認真看下下面的示例程式碼,避免以後寫程式碼時踩坑。

示例一

先一起來看下這段程式碼,你感覺有沒有問題呢?

type IPeople interface {      hello()  }  type People struct {  }    func (p *People) hello() {      fmt.Println("github.com/meetbetter")  }    func errFunc1(in int) *People {      if in == 0 {          fmt.Println("importantFunc返回了一個nil")          return nil      } else {          fmt.Println("importantFunc返回了一個非nil值")          return &People{}      }    }    func main() {      var i IPeople        in := 0        i = errFunc1(in)        if i == nil {            fmt.Println("哈,外部接收到也是nil")      } else {            fmt.Println("咦,外部接收到不是nil哦")          fmt.Printf("%v, %Tn", i, i)      }    }

這段程式碼的執行結果是:

importantFunc返回了一個nil  咦,外部接收到不是nil哦  <nil>, *main.People

可以看到在main函數中收到的返回值不是nil, 明明在errFunc1()函數中返回的是nil,到了main函數為什麼收到的不是nil呢?
這是因為:將nil賦值給*People後再將*People賦值給interface,*People本身是是個指向nil的指針,但是將其賦給介面時只是介面中的值為nil,但是介面中的類型資訊為*main.People而不是nil,所以這個介面不是nil。
是的,Golang中的interface類型包含兩部分資訊——值資訊和類型資訊,只有interface的值合併類型都為nil時interface才為nil,interface底層實現可以在後面的源碼分析看到。

先來看看正確的處理介面返回值的方法,是直接將nil賦給interface:

  func rightFunc(in int) IPeople {      if in == 0 {          fmt.Println("importantFunc返回了一個nil")          return nil      } else {          fmt.Println("importantFunc返回了一個非nil值")          return &People{}      }    }

示例二

下面的程式碼更清晰的證明了一個包含nil指針的介面不是nil介面的結論:

type IPeople interface {      hello()  }  type People struct {  }    func (p *People) hello() {      fmt.Println("github.com/meetbetter")  }    //錯誤:將nil的people給空介面後介面就不為nil,因為interface中的value為nil但type不為nil    func errFunc() *People {        return nil  }    //正確處理返回nil給介面的方法,返回時go就確定了介面是不是nil  func rightFunc() IPeople {        return nil  }  func main() {        var i IPeople      i = errFunc()      if i == nil { //想通過介面是否為nil來判斷故障,卻始終判斷介面非空            fmt.Println("errFunc對了哦,外部接收到也是nil")          fmt.Println(reflect.TypeOf(i))      } else {            fmt.Println("errFunc錯了咦,外部接收到不是nil哦")          fmt.Println(reflect.TypeOf(i))      }        i = rightFunc()      if i == nil {            fmt.Println("rightFunc對了哦,外部接收到也是nil")          fmt.Println(reflect.TypeOf(i))      } else {            fmt.Println("rightFunc錯了咦,外部接收到不是nil哦")          fmt.Println(reflect.TypeOf(i))        }    }

輸出結果:

errFunc錯了咦,外部接收到不是nil哦  *main.People  rightFunc對了哦,外部接收到也是nil  <nil>

interface底層實現

下面的注釋資訊來自參考文章中,從interface底層實現可以看出iface比eface 中間多了一層itab結構, itab 存儲_type資訊和[]fun方法集,所以即使data指向了nil 並不代表interface 就是nil, 還要考慮_type資訊。

type eface struct {      //空介面      _type *_type         //類型資訊      data  unsafe.Pointer //指向數據的指針(go語言中特殊的指針類型unsafe.Pointer類似於c語言中的void*)  }  type iface struct {      //帶有方法的介面      tab  *itab           //存儲type資訊還有結構實現方法的集合      data unsafe.Pointer  //指向數據的指針(go語言中特殊的指針類型unsafe.Pointer類似於c語言中的void*)  }  type _type struct {      size       uintptr  //類型大小      ptrdata    uintptr  //前綴持有所有指針的記憶體大小      hash       uint32   //數據hash值      tflag      tflag      align      uint8    //對齊      fieldalign uint8    //嵌入結構體時的對齊      kind       uint8    //kind 有些枚舉值kind等於0是無效的      alg        *typeAlg //函數指針數組,類型實現的所有方法      gcdata    *byte      str       nameOff      ptrToThis typeOff  }  type itab struct {      inter  *interfacetype  //介面類型      _type  *_type          //結構類型      link   *itab      bad    int32      inhash int32      fun    [1]uintptr      //可變大小 方法集合  }

以上完整程式碼均整理在Github-跟著示例程式碼學Golang項目

參考文章:

Golang第一大坑

"一個包含nil指針的介面不是nil介面"的討論