Go nil

admin
admin 2020年04月04日
  • 在其它设备中阅读本文章

nil是 Go 中的一个使用频率很高的预声明标识符。很多种类的类型的零值都用 nil 表示。很多有其它语言编程经验的程序员在初学 Go 语言的时候常将 nil 看成是其它语言中的 null 或者 NULL。这种看法只是部分上正确的,但是 Go 中的 nil 和其它语言中的 null 或者 NULL 也是有很大的区别的。

一、nil 是一个预声明的标识符

在 Go 中,预声明的 nil 可以表示下列种类(kind)的类型的零值:

  • 指针类型(包括类型安全和非类型安全指针)
  • 映射类型
  • 切片类型
  • 函数类型
  • 通道类型
  • 接口类型

二、预声明标识符 nil 没有默认类型

Go 中其它的预声明标识符都有各自的默认类型,比如

  • 预声明标识符 true 和 false 的默认类型均为内置类型 bool。
  • 预声明标识符 iota 的默认类型为内置类型 int。
    但是,预声明标识符 nil 没有一个默认类型,尽管它有很多潜在的可能类型。事实上,预声明标识符 nil 是 Go 中唯一一个没有默认类型的类型不确定值。我们必须在代码中提供足够的信息以便让编译器能够推断出一个类型不确定的 nil 值的期望类型。

    package main
    func main() {
      // 代码中必须提供充足的信息来让编译器推断出某个nil的类型。
      _ = (*struct{})(nil)
      _ = []int(nil)
      _ = map[int]bool(nil)
      _ = chan string(nil)
      _ = (func())(nil)
      _ = interface{}(nil)
      // 下面这一组和上面这一组等价。
      var _ *struct{} = nil
      var _ []int = nil
      var _ map[int]bool = nil
      var _ chan string = nil
      var _ func() = nil
      var _ interface{} = nil
      // 下面这行编译不通过。
      var _ = nil
    }

三、nil 值尺寸

不同种类的类型的 nil 值的尺寸很可能不相同, 一个类型的所有值的内存布局都是一样的,此类型 nil 值也不例外(假设此类型的零值使用 nil 表示)。所以同一个类型的 nil 值和非 nil 值的尺寸是一样的。但是不同类型的 nil 值的尺寸可能是不一样的。

package main
import (
    "fmt"
    "unsafe"
)
func main() {
    var p *struct{} = nil
    fmt.Println( unsafe.Sizeof( p ) ) // 8
    var s []int = nil
    fmt.Println( unsafe.Sizeof( s ) ) // 24
    var m map[int]bool = nil
    fmt.Println( unsafe.Sizeof( m ) ) // 8
    var c chan string = nil
    fmt.Println( unsafe.Sizeof( c ) ) // 8
    var f func() = nil
    fmt.Println( unsafe.Sizeof( f ) ) // 8
    var i interface{} = nil
    fmt.Println( unsafe.Sizeof( i ) ) // 16
}

上例中的输出是使用官方标准编译器编译并在 64 位的系统架构上运行的结果。在 32 位的系统架构上,这些输出值将减半。

四、nil 值比较

两个不同类型的 nil 值可能不能相互比较

// error: 类型不匹配
var _ = (*int)(nil) == (*bool)(nil)
// error: 类型不匹配
var _ = (chan int)(nil) == (chan bool)(nil)

同一个类型的两个 nil 值可能不能相互比较
在 Go 中,映射类型、切片类型和函数类型是不支持比较类型。比较同一个不支持比较的类型的两个值(包括 nil 值)是非法的。

// 下面的几个比较都编译不通过
var _ = ([]int)(nil) == ([]int)(nil)
var _ = (map[string]int)(nil) == (map[string]int)(nil)
var _ = (func())(nil) == (func())(nil)

但是,映射类型、切片类型和函数类型的任何值都可以和类型不确定的裸 nil 标识符比较。

// 这几行编译都没问题。
var _ = ([]int)(nil) == nil
var _ = (map[string]int)(nil) == nil
var _ = (func())(nil) == nil

两个 nil 值可能并不相等
如果可被比较的两个 nil 值中的一个的类型为接口类型,而另一个不是,则比较结果总是 false。原因是,在进行此比较之前,此非接口 nil 值将被转换为另一个 nil 值的接口类型,从而将此比较转化为两个接口值的比较。从接口一文中,我们得知每个接口值可以看作是一个包裹非接口值的盒子。一个非接口值被转换为一个接口类型的过程可以看作是用一个接口值将此非接口值包裹起来的过程。一个 nil 接口值中什么也没包裹,但是一个包裹了 nil 非接口值的接口值并非什么都没包裹。一个什么都没包裹的接口值和一个包裹了一个非接口值(即使它是 nil)的接口值是不相等的。

fmt.Println( (interface{})(nil) == (*int)(nil) ) // false

五、nil 值访问

访问 nil 映射值的条目不会产生恐慌
访问一个 nil 映射将得到此映射的类型的元素类型的零值。

fmt.Println( (map[string]int)(nil)["key"] ) // 0
fmt.Println( (map[int]bool)(nil)[123] )     // false
fmt.Println( (map[int]*int64)(nil)[123] )   // <nil>

range 关键字后可以跟随 nil 通道、nil 映射、nil 切片和 nil 数组指针

  • 遍历 nil 映射和 nil 切片的循环步数均为零。
  • 遍历一个 nil 数组指针的循环步数为对应数组类型的长度。(但是,如果此数组类型的长度不为零并且第二个循环变量未被舍弃或者忽略,则对应 for-range 循环将导致一个恐慌。)
  • 遍历一个 nil 通道将使当前协程永久阻塞。

比如,下面的代码将输出 0、1、2、3 和 4 后进入阻塞状态。Hello、world 和 Bye 不会被输出。

for range []int(nil) {
    fmt.Println("Hello")
}
for range map[string]string(nil) {
    fmt.Println("world")
}
for i := range (*[5]int)(nil) {
    fmt.Println(i)
}
for range chan bool(nil) { // 阻塞在此
    fmt.Println("Bye")
}

六、nil 值的非接口属主方法调用不会造成恐慌

package main
type Slice []bool
func (s Slice) Length() int {
    return len(s)
}
func (s Slice) Modify(i int, x bool) {
    s[i] = x // panic if s is nil
}
func (p *Slice) DoNothing() {
}
func (p *Slice) Append(x bool) {
    *p = append(*p, x) // 如果p为空指针,则产生一个恐慌。
}
func main() {
    // 下面这几行中的选择器不会造成恐慌。
    _ = ((Slice)(nil)).Length
    _ = ((Slice)(nil)).Modify
    _ = ((*Slice)(nil)).DoNothing
    _ = ((*Slice)(nil)).Append
    // 这两行也不会造成恐慌。
    _ = ((Slice)(nil)).Length()
    ((*Slice)(nil)).DoNothing()
    // 下面这两行都会造成恐慌。但是恐慌不是因为nil
    // 属主实参造成的。恐慌都来自于这两个方法内部的
    // 对空指针的解引用操作。
    /*
    ((Slice)(nil)).Modify(0, true)
    ((*Slice)(nil)).Append(true)
    */
}

事实上,上面的 Append 方法实现不完美。我们应该像下面这样实现之:

func (p *Slice) Append(x bool) {
    if p == nil {
        *p = []bool{x}
        return
    }
    *p = append(*p, x)
}

七、总结

在 Go 中,为了简单和方便,nil被设计成一个可以表示成很多种类型的 零值 预声明标识符。换句话说,它可以表示很多内存布局不同的值,而不仅仅是一个值。