nil 指针错误
阅读提示
- 适用版本:Go 1.21+(示例在 Go 1.22 下验证)· 前置知识:数据类型 · 阅读时长:约 10 分钟
引子
在某一次编写代码的过程中,我需要调用Close()方法来关闭多个对象,就像下面的代码一样
type A struct {
b B
c C
d D
}
func (a A) Close() error {
if a.b != nil {
if err := a.b.Close(); err != nil {
return err
}
}
if a.c != nil {
if err := a.c.Close(); err != nil {
return err
}
}
if a.d != nil {
if err := a.d.Close(); err != nil {
return err
}
}
return nil
}但写这么多if判断感觉不太优雅,B,C和D都实现了Close方法,应该可以更简洁一点,于是我把它们放进了一个切片中,然后循环判断
func (a A) Close() error {
closers := []io.Closer{
a.b,
a.c,
a.d,
}
for _, closer := range closers {
if closer != nil {
if err := closer.Close(); err != nil {
return err
}
}
}
return nil
}这样看起来似乎要更好一点,那么运行一下看看
func main() {
var a A
if err := a.Close(); err != nil {
panic(err)
}
fmt.Println("success")
}结果出乎意料,居然崩了,错误信息如下,意思就是不能对nil接收者调用方法,循环中的if closer != nil似乎没有起到过滤作用,
panic: value method main.B.Close called using nil *B pointer上面这个例子是笔者曾经遇到过的一个 bug 的简化版,很多初学者刚开始可能都会和我一样犯这种错误,下面就来讲讲到底是怎么个回事。
接口
在之前的章节提到过,nil是引用类型的零值,比如切片,map,通道,函数,指针,接口的零值。对于切片,map,通道,函数,可以将它们都看作是指针,都是由指针指向具体的实现。

但唯独接口不一样,接口由两个东西组成:类型和值

当试图对一个变量赋值nil时,会无法通过编译,并且提示如下信息
use of untyped nil in assignment内容大致为不能声明一个值为untyped nil的变量。既然有untyped nil,相对的就肯定会有typed nil,而这种情况往往出现在接口身上。看下面一个简单的例子
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(pa)
fmt.Println(pa == nil)
}输出
<nil>
true
<nil>
false结果非常奇怪,明明pa的输出就是nil,但它就是不等于nil,我们可以通过反射来看看它到底是什么
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.TypeOf(pa))
fmt.Println(reflect.ValueOf(pa))
}输出
<nil>
true
*int
<nil>从结果可以看到,它实际上是(*int)(nil),也就是说pa存储的类型是*int,而它实际的值是nil,当对一个接口类型的值进行相等运算的时候,首先会判断它们的类型是否相等,如果类型不相等,则直接判定为不相等,其次再去判断值是否相等,这一段的接口判断的逻辑可以参考自cmd/compile/internal/walk.walkCompare函数。
所以,如果想要一个接口等于nil,必须要它的值为nil,并且类型也为nil,因为接口中的类型实际上也是一个指针
type iface struct {
tab *itab
data unsafe.Pointer
}如果想要绕开类型,直接判断其值是否为nil,可以使用反射,下面是一个例子
func main() {
var p *int
fmt.Println(p)
fmt.Println(p == nil)
var pa any
pa = p
fmt.Println(reflect.ValueOf(pa).IsNil())
}通过IsNil()可以直接判断其值是否为nil,这样一来就不会出现上述的问题了。所以在平时使用的过程中,假设函数的返回值是一个接口类型,如果你想返回一个零值,最好直接返回nil,不要返回任何具体实现的零值,就算它实现了该接口,但它永远也不会等于nil,这就可能导致例子里面的错误。
小结
解决了上面的问题,接下来看看下面这几个例子
当结构体的接收者为指针接收者时,nil是可用的,看下面一个例子
type A struct {
}
func (a *A) Do() {
}
func main() {
var a *A
a.Do()
}这段代码可以正常运行,并且不会报空指针错误。
当切片为nil的时候,可以访问它的长度和容量,也可以对其添加元素
func main() {
var s []int
fmt.Println(len(s))
fmt.Println(cap(s))
s = append(s, 1)
}当 map 为nil的时候,还可以对其进行访问,但nil的 map 是只读的,一旦尝试写入就会引发panic
func main() {
var s map[string]int
i, ok := s[""]
fmt.Println(i, ok)
fmt.Println(len(s))
// 尝试写入时,会引发panic
s["a"] = 1 // panic: assignment to entry in nil map
}上面例子中的这些有关于nil的特性可能会让人比较困惑,尤其是对于 go 的初学者而言,nil代表着上面几种类型的零值,也就是默认值,默认值应当表现出默认的行为,这也正是 go 的设计者所希望看到的:让nil变得更有用,而不是直接抛出空指针错误。这一理念同样也体现在标准库中,比如开启一个 HTTP 服务器可以这样写
http.ListenAndServe(":8080", nil)我们可以直接传入一个nil Handler,然后http库就会使用默认的Handler来处理 HTTP 请求。
TIP
感兴趣的可以看看这个视频Understanding nil - Gopher Conference 2016,讲的非常清晰易懂。
完整示例
下面的程序可以直接运行:复制到文件后执行 go run main.go,或粘贴到 Go Playground 运行。
package main
import "fmt"
type MyError struct{}
func (e *MyError) Error() string { return "my error" }
func returnsNil() error { return nil }
func returnsTypedNil() error {
var e *MyError
return e // 带类型的 nil:接口本身并不是 nil
}
func main() {
fmt.Println(returnsNil() == nil)
err := returnsTypedNil()
fmt.Println(err == nil)
if err != nil {
fmt.Println("not nil:", err)
}
}预期输出:
true
false
not nil: my error常见错误与踩坑
- nil 接口与"接口中装了一个类型化 nil 指针"是两回事。
returnsTypedNil()返回的error里装着(*MyError)(nil),所以err == nil为false。 - 对 nil 指针调用方法是否安全,取决于方法实现。 值接收者会解引用指针,直接 panic。
- nil map 与 nil 切片的差异要记牢。 nil map 可以读、写入会 panic;nil 切片可以直接
append。 - nil 管道会永久阻塞。 对 nil 管道发送或接收会让协程一直挂起;在
select中该分支永远不会被选中。 - 接口相等比较的是动态类型与值。 两个不同类型的 nil 指针放进接口后并不相等。
更新记录
- 2026-10-14:新增"阅读提示"、完整可运行示例(含预期输出)、常见错误与踩坑清单,以及本更新记录。
