最近在專案中踩了一個深坑——“Golang中一個包含nil指標的介面不是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, %T\n", 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 {
var p *People
return p
}
//正確處理返回nil給介面的方式:直接將nil賦給interface
func rightFunc() IPeople {
var p *People
return p
}
func main() {
if errFunc() == nil {
fmt.Println("對了哦,外部接收到也是nil")
} else {
fmt.Println("錯了咦,外部接收到不是nil哦")
}
if rightFunc() == nil {
fmt.Println("對了哦,外部接收到也是nil")
} else {
fmt.Println("錯了咦,外部接收到不是nil哦")
}
}
輸出結果:
對了哦,外部接收到也是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第一大坑