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{}

中文博客导航
萌ICP备20213456号