Go 杂谈——interface与nil的细节让我出了线上BUG

635 阅读3分钟

首发:mp.weixin.qq.com/s/mA9C\_3eg…

前段时间上了一次线,一开始运行的好好的,但是突然有一天,出现了一个 panic 错误。这可给我吓得不轻,要知道线上的 go 程序 panic 可是很要命的。

但是追查下来,让我百思不得姐。下面我来把现场用一个 demo 复述一下。

type A interface {    Close()}type B struct {}func (*B) Close() {}func main() {    var (        b *B = nil        a A = b    )        if a != nil {        a.Close()    }}

上面的代码没有逻辑,仅仅是复现一下当时的情景。上面这段代码,最终在 18 行 panic 了。

是不是非常奇怪,明明 b 已经是 nil 了,为什么还会进入条件判断,莫非 a 不是 nil?带着这个疑问,我翻阅了一下源码,发现了这两个结构体。

type iface struct {   tab  *itab   data unsafe.Pointer}type eface struct {   _type *_type   data  unsafe.Pointer}

这两个东西就厉害了,它们正是 interface 的原形。iface 定义了有方法的 interface,eface 定义了无方法的 interface,也就是“empty interface”。当 var a A 这样定义时,实际上 a 是被定义成了 eface 这个结构体。所以,实际上 a 并不是 nil,它只是 eface.data 是 nil。

当然,从汇编中也能找到端倪。

0x002f 00047 (main.go:14)  MOVQ   $0, "".b+56(SP)
0x0038 00056 (main.go:15)  MOVQ   $0, ""..autotmp_2+72(SP)
0x0041 00065 (main.go:15)  LEAQ   type.*"".B(SB), AX
0x0048 00072 (main.go:15)  MOVQ   AX, "".a+80(SP)
0x004d 00077 (main.go:15)  MOVQ   $0, "".a+88(SP)

可以看到 b 存储的是 0,而 a 中存储了结构体内的两个指针。

另一个栗子

那么问题来了,在 go 中,我们会经常判断 if err != nil,这个会不会也有问题呢?再一次来个 demo。

func main() {   var err error = nil   fmt.Println(err == nil)}

这个 demo 将 err 直接赋值为 nil。我们来猜猜下面会输出什么呢。

答案就是 true。果然,我们判断 if err != nil 的时候,在这种情况下,确实没有问题。(当然,大部分 err 的返回也是这样返回的。)

同样的,我们再次来看看这段代码编译后的结果。

0x0021 00033 (main.go:6)   XORPS  X0, X0
0x0024 00036 (main.go:6)   MOVUPS X0, "".err+64(SP)

这样,可以清楚的看到,err 直接被赋值成 0(也就是 nil 了)。所以,上面在比较的时候,err 确实是 0 (nil) 了。

解决方案

在目前的 Go 版本中,我并没有找到优雅的解决方案。只能给出三个这种的方案。

第一种,直接用反射来判断。

func IsNil(a A) bool {    return reflect.ValueOf(a).IsNil()}

另一种,便是在定义接口的时候就定义上 IsNil 这个方法。

type A interface {    IsNil() bool}type B struct {}func (b *B) IsNil() bool {    return b == nil}

这样,就能使用 a.IsNil() 来判断了。

第三种,在每个指针接收者方法中判断接收者是不是 nil。

type A interface {    IsNil() bool}type B struct {}func (b *B) Close() {    if b == nil {        return    }}

虽然这三个办法都能解决 nil 的问题,但是,对于鸭子模型的 Go 来说,后两者并不友好,毕竟需要侵入实现的函数。而鸭子模型最大的魅力在于,我们可以不用关心实现,只需要定义接口。