Go 中值为 nil 的 interface

该知识点在之前的博客中曾经提到过,但是我仍然义无反顾踩了坑。排查耗时: 2 天(中间穿插了其他事情)

Go nil 定义

nil 不是一个关键字,你甚至可以 var nil = 1(就像 #define true false 一样),可以将所有 Go 的变量理解成一个 {Type, Value} 的数据。那么自然 nil 也是存在类型的。
不同类型的 nil 不同,运行时会自动把代码中的 nil 替换为与它比较的变量类型的 nil,也即

var a *int
fmt.Println(a == nil) // fmt.Println(a == (*int)(nil))

易犯的错误

那么,如果程序无法推测出类型、或者推测类型错误,比较结果自然也是错误的

可以看下面的程序

package main

import (
	"fmt"
)

type AnimalInterface interface {
	Eat()
}

type DogInterface interface {
	AnimalInterface
	Woof()
}

type Husky struct{}

func (dog *Husky) Eat()  {}
func (dog *Husky) Woof() { fmt.Println("Woof!") }

func main() {
	var animal AnimalInterface = nil
	var dog DogInterface = nil
	var husky *Husky = nil
	fmt.Printf("%#v %#v %#v %#v %#v %#v\n", animal, dog, husky, animal != nil, dog != nil, husky != nil)

	animal = (*Husky)(nil)
	dog = (*Husky)(nil)
	husky = (*Husky)(nil)
	fmt.Printf("%#v %#v %#v %#v %#v %#v\n", animal, dog, husky, animal != nil, dog != nil, husky != nil)

	animal = (DogInterface)(nil)
	dog = (DogInterface)(nil)
	fmt.Printf("%#v %#v %#v %#v %#v %#v\n", animal, dog, husky, animal != nil, dog != nil, husky != nil)

	animal = new(Husky)
	dog = new(Husky)
	husky = new(Husky)
	fmt.Printf("%#v %#v %#v %#v %#v %#v\n", animal, dog, husky, animal != nil, dog != nil, husky != nil)
}

该代码编译器会提示 Warning,但是如果套娃几层后可能会被编译器忽视

这段代码的输出为

<nil> <nil> (*main.Husky)(nil) false false false
(*main.Husky)(nil) (*main.Husky)(nil) (*main.Husky)(nil) true true false
<nil> <nil> (*main.Husky)(nil) false false false
&main.Husky{} &main.Husky{} &main.Husky{} true true true

可以观察到,interface{} 实际上是 没有类型

第一部分由于都是 nil,因此会被推断为变量对应的类型,获得正确的比较结果
而第二行由于 animaldog 都被赋值了 (*Husky)(nil),而他们本身的类型推断为 (nil)(nil),因此在比较结果时返回的是 true(因为 Husky 符合 Animal 和 Dog 的约束,因此是可以赋值成功的。
而由于 interface{} 类型都是 nil,因此第三行正确
第四行都被赋值,不属于 nil

实际项目中可能的错误

虽然看起来该问题很容易理解,但是仍然属于一不留神就会犯的错误。

考虑如下场景

函数内需要检查传入的参数是否合法,对于值为 nil 的参数,应该执行特殊的初始化逻辑。被传入的函数需要根据别的条件来确定是否存在,因此先初始化为 nil 的对应结构体,再根据需要对其赋值
一个类似的 DEMO 如下

func run(r *io.Reader) {
    if r == nil {
        return
    }
    r.Read()
}

func Run(hasReader bool) {
    var r *bytes.Buffer = nil
    if hasReader {
        r = new(bytes.Buffer)
    }
    run(r)
}

虽然看上去明确赋值 r = nil,而且在不需要时避免初始化无用的结构体,节省了内存。看上去一切美好,但是 r 只会等于 (*bytes.Buffer)(nil) 永远不会不会等于 nil。很容易就会发生空指针异常。

因此很多函数在处理时,会直接返回 nil 来避免出错,如

func do() (*Object, error) {
    // ...
    if err != nil {
        return nil, err
    }

    return &Object{}, nil
}

在这里,尽管最后 return &Object, err 结果上也是返回了一个为 nil 的错误,但由于 error 本身是一个 interface{},因此外层判断时可能会出错。故,必须直接返回 nil

结论

如果返回值是 nil,那么直接写 nil,而非某个结构体指针(即使这个指针目前指向 nil

类似地,尽可能避免使用指向 nil 的结构体指针,取而代之使用对应的指向 nilinterface{}

参考资料