nil
nil
引子
在某一次编写代码的过程中,我需要调用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 请求。
提示
感兴趣的可以看看这个视频Understanding nil - Gopher Conference 2016,讲的非常清晰易懂。
