接口值等于 nil 的条件是“类型字与数据字都为空”。当一个类型化的 nil 指针被装进接口时,类型字不再为空,于是 err != nil 为真,而打印出来又是
源码(省略 main 与辅助):
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "reflect"
6 | "unsafe"
7 | )
8 |
9 | type MyErr struct{ Code int }
10 |
11 | func (e *MyErr) Error() string { return fmt.Sprintf("myerr %d", e.Code) }
12 |
13 | // 返回具体指针类型再赋给 error:fail=false 时返回的是 nil 指针
14 | func buggy(fail bool) error {
15 | var e *MyErr
16 | if fail {
17 | e = &MyErr{Code: 7}
18 | }
19 | return e
20 | }
21 |
22 | func fixed(fail bool) error {
23 | if fail {
24 | return &MyErr{Code: 7}
25 | }
26 | return nil
27 | }
28 |
29 | type words struct{ tab, data unsafe.Pointer }
buggy(false):err == nil → false;%T = *main.MyErr;%v = <nil>
fixed(false):err == nil → true;%T = <nil>;%v = <nil>
buggy 的两个字:tab == nil?false,data == nil?true;fixed 的两个字:tab == nil?true,data == nil?true
go vet 对这份代码:rc=0,输出:(无任何警告)问题出在 return e:e 的静态类型是 *MyErr,装进 error 时编译器按 (*MyErr)(nil) 做了一次转换,接口的类型字(这里是 itab 指针)被填上了。用上一篇的方法读两个字,看得清清楚楚:buggy 的 tab 不为空、data 为空。解决办法很朴素:没有错误时直接 return nil,不要先装进具体类型的变量再返回。
var p *MyErr:p == nil → true var e error:e == nil → true var any1 any:any1 == nil → true var any2 any = p(nil 的 *MyErr):any2 == nil → false;any2 == (*MyErr)(nil) → true;any2 == any(nil) → false var e3 error = p:e3 == nil → false;e3 == (*MyErr)(nil) → true
规则可以压缩成一句:接口值只有在两个字都是空时才等于 nil。比较 any2 == (*MyErr)(nil) 为真,是因为把右边也转成了同样的接口值再比较(类型字相同、数据字都是 nil)。
e1.Error()(指针接收者,解引用 e.Code) panic:runtime error: invalid memory address or nil pointer dereference 返回: <nil NilSafe> 断言 ok = true,v == nil → true
非 nil 接口里装着 nil 指针,方法调用是合法的,会照常分派到 (*MyErr).Error。要不要出事,看方法体:解引用了接收者就 panic,先判断了 n == nil 就没事。这也是为什么这个陷阱常常在“不出错的路径”上才触发 panic。
isNil(buggy(false)) → true;isNil(fixed(false)) → true;isNil(&MyErr{}) → false;isNil(0) → false
reflect.ValueOf(0).IsNil() panic:reflect: call of reflect.Value.IsNil on int Value
nil map / nil slice / nil func 装进 any:== nil → false / false / false;isNil → true / true / true最好的办法是不要制造这种值。不得已要判断时,reflect 的 IsNil 适用于 Pointer、Map、Slice、Func、Chan、Interface 这几种 Kind,其它 Kind 调用会 panic(上面实测 reflect.ValueOf(0).IsNil() 就是),所以先按 Kind 筛选。反射有成本,不要放在热路径上。