Go接口:为什么nil接口不等于nil

接口的静态特性与动态特性

接口的静态特性体现在接口类型变量具有静态类型,比如var err error中变量 err 的静态类型为 error。拥有静态类型,那就意味着编译器会在编译阶段对所有接口类型变量的赋值操作进行类型检查,编译器会检查右值的类型是否实现了该接口方法集合中的所有方法。如果不满足,就会报错:

var err error = 1 // cannot use 1 (type int) as type error in assignment: int does not implement error (missing Error method)

而接口的动态特性,体现在接口类型变量在运行时还存储了右值的真实类型信息,这个右值信息被称为接口类型变量的动态类型

var err error
err = errors.New("error1")
fmt.Printf("%T\n", err)  // *errors.errorString

nil error值 != nil

先来看一段代码:

type MyError struct {
    error
}

var ErrBad = MyError{
    error: errors.New("bad things happened"),
}

func bad() bool {
    return false
}

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = &ErrBad
    }
    return p
}

func main() {
    err := returnsError()
    if err != nil {
        fmt.Printf("error occur: %+v\n", err)
        return
    }
    fmt.Println("ok")
}

运行结果如下:

error occur: <nil>

按照我们的期望,程序最终应该会输出ok,但实际上是却进入了错误处理分支。
要想弄清楚这个问题,我们需要进一步了解接口类型变量的内部表示。

接口类型变量的表示

我们可以在$GOROOT/src/runtime/runtime2.go中找到接口类型变量在运行时的表示:

// $GOROOT/src/runtime/runtime2.go
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type eface struct {
    _type *_type
    data  unsafe.Pointer
}
  • eface 用于没有方法的空接口(empty interface)类型变量,也就是interface{}类型变量;
  • iface 用于表示其余拥有方法的接口interface类型变量。
    可以看到他们之间的共同点是都有两个指针类型的变量。

在看看两个不同指针的内部结构:
_type

// $GOROOT/src/runtime/type.go

type _type struct {
    size       uintptr
    ptrdata    uintptr // size of memory prefix holding all pointers
    hash       uint32
    tflag      tflag
    align      uint8
    fieldAlign uint8
    kind       uint8
    // function for comparing objects of this type
    // (ptr to object A, ptr to object B) -> ==?
    equal func(unsafe.Pointer, unsafe.Pointer) bool
    // gcdata stores the GC type data for the garbage collector.
    // If the KindGCProg bit is set in kind, gcdata is a GC program.
    // Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
    gcdata    *byte
    str       nameOff
    ptrToThis typeOff
}

itab

// $GOROOT/src/runtime/runtime2.go
type itab struct {
    inter *interfacetype
    _type *_type
    hash  uint32 // copy of _type.hash. Used for type switches.
    _     [4]byte
    fun   [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}

可以看到事实上itab里其实也有一个_type类型的指针,只不过还封装几个其他类型的变量,我们要关注的就是这个_type,因为我们判断两个接口变量是否相等,只需要判断 _type/tab 是否相同,以及 data 指针指向的内存空间所存储的数据值是否相同就可以了。这里要注意不是 data 指针的值相同。

第一种:nil接口变量

func printNilInterface() {
  // nil接口变量
  var i interface{} // 空接口类型
  var err error     // 非空接口类型
  println(i)
  println(err)
  println("i = nil:", i == nil)
  println("err = nil:", err == nil)
  println("i = err:", i == err)
}
//输出:
	(0x0,0x0)
	(0x0,0x0)
	i = nil: true
	err = nil: true
	i = err: true

无论是空接口还是非空接口类型,单纯的生命而没有初始化的值都是nil,则其内部的都类型信息和数据值信息都为空。

第二种:空接口类型变量

func printEmptyInterface() {
    var eif1 interface{} // 空接口类型
    var eif2 interface{} // 空接口类型
    var n, m int = 17, 18

    eif1 = n
    eif2 = m

    println("eif1:", eif1)
    println("eif2:", eif2)
    println("eif1 = eif2:", eif1 == eif2) // false

    eif2 = 17
    println("eif1:", eif1)
    println("eif2:", eif2)
    println("eif1 = eif2:", eif1 == eif2) // true

    eif2 = int64(17)
    println("eif1:", eif1)
    println("eif2:", eif2)
    println("eif1 = eif2:", eif1 == eif2) // false
}
//输出:
	eif1: (0x10ac580,0xc00007ef48)
	eif2: (0x10ac580,0xc00007ef40)
	eif1 = eif2: false
	eif1: (0x10ac580,0xc00007ef48)
	eif2: (0x10ac580,0x10eb3d0)
	eif1 = eif2: true
	eif1: (0x10ac580,0xc00007ef48)
	eif2: (0x10ac640,0x10eb3d8)
	eif1 = eif2: false

对于空接口类型变量,只有_type和data所指数据内容一致的情况下,两个空接口类型变量之间才能划等号。 另外,Go 在创建 eface 时一般会为 data 重新分配新内存空间,将动态类型变量的值复制到这块内存空间,并将 data 指针指向这块内存空间。因此我们多数情况下看到的 data 指针值都是不同的。

第三种:非空接口类型

type T int

func (t T) Error() string { 
    return "bad error"
}

func printNonEmptyInterface() { 
    var err1 error // 非空接口类型
    var err2 error // 非空接口类型
    err1 = (*T)(nil)
    println("err1:", err1)
    println("err1 = nil:", err1 == nil)

    err1 = T(5)
    err2 = T(6)
    println("err1:", err1)
    println("err2:", err2)
    println("err1 = err2:", err1 == err2)

    err2 = fmt.Errorf("%d\n", 5)
    println("err1:", err1)
    println("err2:", err2)
    println("err1 = err2:", err1 == err2)
}   
//输出:
	err1: (0x10ed120,0x0)
	err1 = nil: false
	err1: (0x10ed1a0,0x10eb310)
	err2: (0x10ed1a0,0x10eb318)
	err1 = err2: false
	err1: (0x10ed1a0,0x10eb310)
	err2: (0x10ed0c0,0xc000010050)
	err1 = err2: false

对于err1 = (*T)(nil)这种情况,println 输出的 err1 是(0x10ed120,0x0),也就是非空接口类型变量的类型信息并不为空,数据指针为空,因此它与nil(0x0,0x0)之间不能划等号。

第四种:空接口类型变量与非空接口类型变量的等值比较

func printEmptyInterfaceAndNonEmptyInterface() {
  var eif interface{} = T(5)
  var err error = T(5)
  println("eif:", eif)
  println("err:", err)
  println("eif = err:", eif == err)

  err = T(6)
  println("eif:", eif)
  println("err:", err)
  println("eif = err:", eif == err)
}
//输出:
	eif: (0x10b3b00,0x10eb4d0)
	err: (0x10ed380,0x10eb4d8)
	eif = err: true
	eif: (0x10b3b00,0x10eb4d0)
	err: (0x10ed380,0x10eb4e0)
	eif = err: false

可以看到,空接口类型变量和非空接口类型变量内部表示的结构有所不同(第一个字段:_type vs. tab),两者似乎一定不能相等。

总结

Go 在进行等值比较时,类型比较使用的是 eface 的 _type 和 iface 的 tab._type,因此就像我们在这个例子中看到的那样,当 eif 和 err 都被赋值为T(5)时,两者之间是划等号的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值