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
的时候,还可以对其进行访问
func main() {
var s map[string]int
i, ok := s[""]
fmt.Println(i, ok)
fmt.Println(len(s))
}
上面例子中的这些有关于nil
的特性可能会让人比较困惑,尤其是对于go的初学者而言,nil
代表着上面几种类型的零值,也就是默认值,默认值应当表现出默认的行为,这也正是go的设计者所希望看到的:让nil
变得更有用,而不是直接抛出空指针错误。这一理念同样也体现在标准库中,比如开启一个HTTP服务器可以这样写
http.ListenAndServe(":8080", nil)
我们可以直接传入一个nil Handler
,然后http
库就会使用默认的Handler
来处理HTTP请求。
提示
感兴趣的可以看看这个视频Understanding nil - Gopher Conference 2016,讲的非常清晰易懂。