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 转换成实例对象后再进行判空