从代码开始
type A interface{}
type B struct{}
var a *B
print(a == nil) //true
print(a == (*B)(nil)) //true
print((A)(a) == (*B)(nil)) //true
print((A)(a) == nil) //false
上面是一段原创的极度反直觉的代码。为什么前面 3 个都相等,最后一个就不相等了呢?
其实在我之前关于 interface 的文章中,是有部分解答的。但是只能回答为什么最后的等式不成立。原因简单说一下,是因为只有当一个 interface 的 value 和 type 都 unset 的时候,它才等于 nil,而上述代码中的 interface (A)a
的 T 是*B
, 不是 unset.
比较难以理解的其实是第三个等式。前两个式子左右两边都是 nil,相等情有可原。第三个式子左边明明不等于 nil 的,为什么会相等呢?下面马上就会回答。
不同的 nil
nil
其实甚至不是 golang 的关键词,只是一个变量名。定义在 buildin/buildin.go 中
// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type
// Type is here for the purposes of documentation only. It is a stand-in
// for any Go type, but represents the same type for any given function
// invocation.
type Type int
换句话说,我们也可以自己声明一个 nil,就会把预定义的 nil 覆盖了。自己试试就好了, 这肯定是不推荐的。根据这里的定义,也可以看出,在 golang 中 nil 代表了pointer
, channel
, func
, interface
, map
或者 slice
的 zero value.
而这里就出现一个问题,这些类型之间千差万别,一个 nil 怎么可以代表这么多类型呢?其实一个 nil 确实有些表达不了,这也是很多误会产生的原因。简单来说,nil 也是有类型的,(*int)(nil)
和(interface{})(nil)
就是两个不同的变量,它们也不相等。更有些类型比如([]int)(nil)
因为是 slice 类型的缘故,根本就是不能比较的。
回到上文。当 interface 与一个值做比较的时候,会同时比较 type 和 value,都相等的时候,才会认为相等。上文中的(*B)(nil)
的 type 与(A)(a)
相同,都是*B
,值也相同,都是nil
,所以他们就相等了。
特别的 nil
尝试一下一行很简单的代码
报错 “use of untyped nil in variable declaration”. 这很好理解,任何变量都应该有类型的,但是 a 的类型是什么呢,编译器百思不得其解,于是它生气了。哄一下应该没用,试着这样改一下就没问题了。
不过上文的错误信息中出现了一个特殊的 nil,”untyped nil”. 当我们直接写一个 nil 出来的时候,它是没有类型的。没有类型的 nil 虽然不能直接赋值给变量,但是可以与一些特定类型的变量进行比较,比如上面出现过的
这是合法的。这是untyped nil
的特别之处,当它被拿来与一个变量进行比较的时候,根据不同的变量,就会有不同的逻辑。
展开说说
为了证明我没有骗人,下面展开说说不同变量类型在什么情况下可以等于 nil
pointer
nil pointer 就是一个没有指向任何值的指针,它的值是 0x0. 做个小实验
var a = (*int)(unsafe.Pointer(uintptr(0x0)))
print(a == nil) //true
恭喜我们人工创造了一个 nil pointer !
slice
一个 slice 由 3 部分组成,pointer,len 和 cap. 这句话其实展开来说很长。如果能看懂下面的代码,那就是大约理解了。
当 pointer 是 nil,len 和 cap 都是 0 的时候,这个 slice 等于 nil. 下面做个实验
var a = []int{}
print(a==nil) //false
type aa struct {
ptr unsafe.Pointer
len int
cap int
}
aaa := (*aa)(unsafe.Pointer(&a))
aaa.ptr = nil
print(a==nil) //true
略微有点黑科技。简单来说,我们原本声明了一个empty slice
, empty slice 是不等于 nil。但是我们把这个 slice 结构体中的 ptr 改成了 nil,于是这个 slice 就变成了nil slice
.
话说,关于 empty slice 和 nil slice 取舍,golang 的官方是推荐大多数情况下都应该用的 nil slice 的,除了是 encoding JSON object 等特殊情况。有点跑题,就不展开说了,具体可以参考这里的官方文档。
chanel & map & func
这 3 位大哥每个都够讲一年的。但是简单来说,它们都是一个指针指向一堆 implementation. 所以就可以把它们看成指针了,这个指针是 nil,那就是 nil 了。
interface
这个已经说过,当一个 interface 的 type 和 value 都是 nil 的时候,这个 interface 才等于 nil. 这真的是个坑人无数的 golang 陷阱,这里就再举一个小栗子好了。
type A interface{}
type B struct{}
var a A = (*B)(nil)
print(a == nil) //false
a = nil
print(a == nil) //true
一个神奇之处
当方法接收者为 nil 时,仍然可以访问对应的方法。偶尔可以帮忙减少代码量。虽然根据方法的写法,是有可能 panic 的。 比如
type A []int
func (a A) Get() int {
return a[0]
}
func main() {
var a A
a.Get()
}
这个代码是可以编译通过的,但是运行时会 panic, runtime error: index out of range [0] with length 0.
小感想
从上面 nil slice 和 empty slice 的区别引出,其实 empty slice 也可以作为 slice 的 zero value。特地发明一个 nil 值,应该是 golang 出于对性能的考虑。nil pointer 其实是一切 nil 值的根本形态,我理解背后的思想就是能不分配的内存就先不分配,pointer 就先让它 nil 着。
名字都是 nil,细想之下区别非常之大,golang 围绕了 nil 制定了了很多固定的特殊用法。因此,在大部分情况下,nil 的使用是非常自然的。这样的设计,究竟是好是坏呢?
参考资料
https://medium.com/@ishagirdhar/nil-in-golang-aaa16565a5be
https://stackoverflow.com/questions/19761393/why-does-go-have-typed-nil
https://github.com/golang/go/wiki/CodeReviewComments#declaring-empty-slices
https://www.youtube.com/watch?v=ynoY2xz-F8s
最后这个 youtube 视频其实讲解得挺详细的,推荐看一看~
https://zhuanlan.zhihu.com/p/151140497