Skip to content

nil 指针错误 ​

阅读提示

  • 适用版本:Go 1.21+(示例在 Go 1.22 下验证)· 前置知识:数据类型 · 阅读时长:约 10 分钟

引子 ​

在某一次编写代码的过程中,我需要调用Close()方法来关闭多个对象,就像下面的代码一样

go
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方法,应该可以更简洁一点,于是我把它们放进了一个切片中,然后循环判断

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

这样看起来似乎要更好一点,那么运行一下看看

go
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,而这种情况往往出现在接口身上。看下面一个简单的例子

go
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,我们可以通过反射来看看它到底是什么

go
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,因为接口中的类型实际上也是一个指针

go
type iface struct {
  tab  *itab
  data unsafe.Pointer
}

如果想要绕开类型,直接判断其值是否为nil,可以使用反射,下面是一个例子

go
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是可用的,看下面一个例子

go
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

go
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 服务器可以这样写

go
http.ListenAndServe(":8080", nil)

我们可以直接传入一个nil Handler,然后http库就会使用默认的Handler来处理 HTTP 请求。

TIP

感兴趣的可以看看这个视频Understanding nil - Gopher Conference 2016,讲的非常清晰易懂。

完整示例 ​

下面的程序可以直接运行:复制到文件后执行 go run main.go,或粘贴到 Go Playground 运行。

go
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)
	}
}

预期输出:

text
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:新增"阅读提示"、完整可运行示例(含预期输出)、常见错误与踩坑清单,以及本更新记录。