bigywater.tech
Go · 14

nil 接口陷阱:为什么返回 nil 指针的 error 不等于 nil

接口值等于 nil 的条件是“类型字与数据字都为空”。当一个类型化的 nil 指针被装进接口时,类型字不再为空,于是 err != nil 为真,而打印出来又是 。这篇把这个现象拆开:读出两个字、对照五种 nil、看方法调用的结果,再给出判断的写法。

false
buggy(false) 没出错时,返回的 error 与 nil 比较的结果
tab ≠ nil
装着 nil *MyErr 的 error:类型字非空、数据字为空
0 个警告
go vet 对这段有问题的代码没有任何输出

经典陷阱

实测 · 本机执行输出
  源码(省略 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,不要先装进具体类型的变量再返回。

五种“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 筛选。反射有成本,不要放在热路径上。

分步演示

nil 接口陷阱
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew);用 go run / go build / go test 直接编译运行,需要第三方库时经代理从 proxy.golang.org 下载;计时用 time.Now / testing.B,数字每次运行会有小幅抖动。用 unsafe 把 error 按 {tab, data} 两个字读出来,只比较是否为 nil;go vet 用的是 Go 1.27.1 自带的默认检查集;其它静态检查工具(staticcheck、golangci-lint 等)本机没有安装,没有试
    • 没有验证:标准库里哪些类型的方法支持 nil 接收者、第三方静态检查工具能否发现这种返回、泛型代码里同样的陷阱、cgo 或 syscall.Errno 等非指针错误类型的行为
    京ICP备2026067502号-1京公网安备11010502063487号