go 语言 interface{} 的易错点

旧巷老猫 提交于 2019-11-28 12:53:31

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

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!