暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

关于Golang中的nil

零君聊软件 2021-07-30
1895

先看一个例子,

    package main


    import "fmt"


    func main() {
    var m map[int]int = nil
    v, 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 int


                      select {
                      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 main


                        import "fmt"


                        type Interface interface {
                        Do()
                        }


                        type MyInt int


                        func (mi MyInt) Do() {
                        fmt.Println("MyInt Do()")
                        }


                        func main() {
                        var i Interface
                        var mi Interface = (*MyInt)(nil)
                        fmt.Println(i == nil)
                        fmt.Println(mi == nil)
                        }


                        输出结果是:

                          true
                          false

                          原因很简单,接口只有type和value都是nil的时候,才等于nil。mi虽然值是nil,但是已经设置了类型,所以并不等于nil。我在一年多前的一篇文章已经阐明过这一点,具体参考:


                          --END--

                          文章转载自零君聊软件,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                          评论