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
,因此会被推断为变量对应的类型,获得正确的比较结果
而第二行由于 animal
和 dog
都被赋值了 (*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
的结构体指针,取而代之使用对应的指向 nil
的 interface{}