Go语言interface 与 nil 的比较

2019-11-10 11:17:39刘景俊

// InterfaceStructure 定义了一个interface{}的内部结构
type InterfaceStructure struct {
 pt uintptr // 到值类型的指针
 pv uintptr // 到值内容的指针
}

// asInterfaceStructure 将一个interface{}转换为InterfaceStructure
func asInterfaceStructure (i interface{}) InterfaceStructure {
 return *(*InterfaceStructure)(unsafe.Pointer(&i))
}

func TestInterfaceStructure(t *testing.T) {
 var i1, i2 interface{}
 var v1 int = 0x0AAAAAAAAAAAAAAA
 var v2 int = 0x0BBBBBBBBBBBBBBB
 i1 = v1
 i2 = v2
 fmt.Printf("sizeof interface{} = %dn", unsafe.Sizeof(i1))
 fmt.Printf("i1 %x %+vn", i1, asInterfaceStructure(i1))
 fmt.Printf("i2 %x %+vn", i2, asInterfaceStructure(i2))
 var nilInterface interface{}
 fmt.Printf("nil interface = %+vn", asInterfaceStructure(nilInterface))
}

这段代码的输出如下:

sizeof interface{} = 16
i1 aaaaaaaaaaaaaaa {pt:5328736 pv:825741282816}
i2 bbbbbbbbbbbbbbb {pt:5328736 pv:825741282824}
nil interface = {pt:0 pv:0}

所以对于一个interface{}类型的nil变量来说,它的两个指针都是0。这是符合Go语言对nil的标准定义的。在Go语言中,nil是零值(Zero Value),而在Java之类的语言里,null实际上是空指针。关于零值和空指针有什么区别,这里就不再展开了。

当我们将一个具体类型的值赋值给一个interface类型的变量的时候,就同时把类型和值都赋值给了interface里的两个指针。如果这个具体类型的值是nil的话,interface变量依然会存储对应的类型指针和值指针。

func TestAssignInterfaceNil(t *testing.T) {
 var p *int = nil
 var i interface{} = p
 fmt.Printf("%v %+v is nil %vn", i, asInterfaceStructure(i), i == nil)
}

输入如下:

<nil> {pt:5300576 pv:0} is nil false

可见,在这种情况下,虽然我们把一个nil值赋值给interface{},但是实际上interface里依然存了指向类型的指针,所以拿这个interface变量去和nil常量进行比较的话就会返回false。

如何解决这个问题

想要避开这个Go语言的坑,我们要做的就是避免将一个有可能为nil的具体类型的值赋值给interface变量。以上述的OpenRedis为例,一种方法是先对OpenRedis返回的结果进行非-nil检查,然后再赋值给interface变量,如下所示。

var storageEngine StorageEngine // 这是一个全局变量
redis := OpenRedis(cfg.Url, dbindex)
if redis != nil {
  // 连接成功
  storageEngine = redis // 确定redis不是nil之后再赋值给interface变量
} else {
  // 连接失败
  ...
}

另外一种方法是让OpenRedis函数直接返回EntityStorage接口类型的值,这样就可以把OpenRedis的返回值直接正确赋值给EntityStorage接口变量。

// OpenRedis opens redis as entity storage
func OpenRedis(url string, dbindex int) EntityStorage {
 c, err := redis.DialURL(url)
 if err != nil {
 return nil
 }

 if dbindex >= 0 {
 if _, err := c.Do("SELECT", dbindex); err != nil {
  return nil
 }
 }

 es := &redisEntityStorage{
 c: c,
 }

 return es
}