先看一个例子,
package mainimport "fmt"func main() {var m map[int]int = nilv, ok := m[3]fmt.Println(v, ok, m == nil)}
估计很多人看到这个例子,第一感觉是应该panic。但实际上不会,运行结果是:
0 false true
本文简要总结一下golang中nil的各种细节以及坑。
nil是什么
Golang中的nil与C++/Java中的NULL/null并不一样。
在C++中NULL其实就是0,在Java中null意味着nothing,任何未初始化的对象都等于null;不管是C++还是Java,访问NULL/null,都会产生空指针异常。
而在Golang中,nil则是一个预定义变量而已,定义在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
既然nil只是一个变量,那你自己也可以再定一个名字为nil的变量,虽然并不推荐这么做。根据上面的注释,不难看出,nil只能用来表示以下类型的零值,nil也只能和这些类型的变量进行比较。
pointer, channel, func, interface, map, or slice
nil和map
首先,当map为nil时,可以用在range从句中,只不过循环次数为0而已。这点在Golang spec中有明确说明:
If the map is nil, the number of iterations is 0.
其次,读nil map也是合法的,例如本文开头的例子,既不会有编译错误,也不会有runtime panic。这样设计的初衷,可能是为了简化程序员的负担,不用担心读nil map会产生panic,因为不用判断nil,所以也可以简化代码。
在Golang spec中也有明确的说明,
A nil map is equivalent to an empty map except that no elements may be added.
在Golang runtime中对应的实现在runtime/map.go中。从下面的代码中,可以看出当map为nil时,是直接返回零值。
if h == nil || h.count == 0 {if t.hashMightPanic() {t.hasher(key, 0) // see issue 23734}return unsafe.Pointer(&zeroVal[0])}
t.hasher(key, 0)是为了防止key是non-comparable,
具体参考:
https://github.com/golang/go/issues/23734
虽然访问nil map不需要任何判断,但是要注意一点,往nil map里写入数据会产生panic!在Golang spec也同样明确说明了,请参考上面贴的Spec中的那句话。在Golang runtime对应的限制同样在runtime/map.go中,
if h == nil {panic(plainError("assignment to entry in nil map"))}
可能有人不假思索马上就问,如果读nil map返回零值,那么怎么判断是到底是数据本来就是零值,还是由于访问了nil或者empty map而返回的零值呢?答案就是通过第二个返回值来判断。
所以,这里其实有一个不大不小的坑,从nil map读数据合法,但是向nil map写数据会产生panic!
一年多以前,我曾经给golang raise了一个issue,但遭到了很多的反对。一个原因是这样改动可能会对现有的代码产生影响,另一个原因就是上面提到的目前设计的优点。
nil和slice
同样,当slice为nil时,可以用在range从句中,循环次数也是0。Golang spec中同样有明确说明:
For a nil slice, the number of iterations is 0
另外,nil slice和nil map都可以做为参数调用len函数,返回均为0。但是slice和map不同的是,试图读nil slice (例如s[0]) 是会产生runtime panic。panic的原因不是因为slice为nil,而是因为index out of range。
nil和channel
channel可以用在range从句中,但是如果channel为nil,那么range从句会永远阻塞,Golang spec里有明确说明:
If the channel is nil, the range expression blocks forever
Golang runtime中对应的实现定义在runtime/chan.go中,
if c == nil {if !block {return}gopark(nil, nil, waitReasonChanReceiveNilChan, traceEvGoStop, 2)throw("unreachable")}
向nil channel里写数据,同样也会block。Golang spec明确指出,nil channel还未准备好通信:
A nil channel is never ready for communication.
Golang runtime中对应的限制为:
if c == nil {if !block {return false}gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2)throw("unreachable")}
细心的读者可能注意到runtime的实现中有一个block变量,如果为false,则会直接返回,那么就不会阻塞。那么在什么情况下,不会阻塞呢?答案就是channel和select配合使用的时候,例如:
func main() {var c chan intselect {case v := <- c:fmt.Printf("channel value: %d\n", v)case <-time.After(5*time.Second):fmt.Println("5 seconds later")}}
最后额外提一句,向已经close的channel写数据,会引起panic!
nil和Interface
关于interface本身,可以单独写很多文章。这里只简单提一点,nil Interface和nil比较时,有可能相等,也有可能并不相等。继续往下阅读之前,请思考一下,下面这个例子输出是什么?
package mainimport "fmt"type Interface interface {Do()}type MyInt intfunc (mi MyInt) Do() {fmt.Println("MyInt Do()")}func main() {var i Interfacevar mi Interface = (*MyInt)(nil)fmt.Println(i == nil)fmt.Println(mi == nil)}
输出结果是:
truefalse
原因很简单,接口只有type和value都是nil的时候,才等于nil。mi虽然值是nil,但是已经设置了类型,所以并不等于nil。我在一年多前的一篇文章已经阐明过这一点,具体参考:
--END--




