9.2 深入理解nil

nil是Go中熟悉且重要的预先声明的标识符。它是多种类型零值的字面表示。许多具有其他一些流行语言经验的新Go程序员可能会将其nil视为null(或NULL)其他语言的对应物 。这部分是正确的,但nil 在Go和null(或NULL)其他语言之间存在许多差异。

按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是””,而指针,函数,interface,slice,channel和map的零值都是nil。

nil 没有默认类型

Go中的每个其他预先声明的标识符都具有默认类型。例如,

  • 默认类型为truefalse 都是bool类型。
  • 默认类型iotaint

但是nil它没有默认类型,尽管它有许多可能的类型。编译器必须有足够的信息来nil从上下文中推导出值的类型 。

示例:

  1. package main
  2. func main() {
  3. // This following line doesn't compile.
  4. /*
  5. v := nil
  6. */
  7. // There must be sufficient information for compiler
  8. // to deduce the type of a nil value.
  9. _ = (*struct{})(nil)
  10. _ = []int(nil)
  11. _ = map[int]bool(nil)
  12. _ = chan string(nil)
  13. _ = (func())(nil)
  14. _ = interface{}(nil)
  15. // This lines are equivalent to the above lines.
  16. var _ *struct{} = nil
  17. var _ []int = nil
  18. var _ map[int]bool = nil
  19. var _ chan string = nil
  20. var _ func() = nil
  21. var _ interface{} = nil
  22. }

nil Go中是一个预先声明的标识符

您可以在nil不声明的情况下使用它。

nil 可以表示多种类型的零值

在Go中,nil可以表示以下类型的零值:

  • pointer types (including type-unsafe ones).
  • map types.
  • slice types.
  • function types.
  • channel types.
  • interface types.

示例:

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. Id int
  5. Name string
  6. Info interface{}
  7. }
  8. func main() {
  9. var p Person
  10. fmt.Println(p)// {0 <nil>}
  11. }

nil 在Go中不是关键字

预先宣布的nil可以被遮蔽。

示例:

  1. package main
  2. import "fmt"
  3. func main() {
  4. nil := 123
  5. fmt.Println(nil) // 123
  6. }

nil具有不同种类的价值的大小可能不同

一个类型的所有值的内存布局总是相同的。 nil类型的值不是例外。nil值的大小始终与其类型与nil值相同的非零值的大小相同。因此,nil表示不同类型的不同零值的标识符可以具有不同的大小。

示例:

  1. package main
  2. import (
  3. "fmt"
  4. "unsafe"
  5. )
  6. func main() {
  7. var p *struct{} = nil
  8. fmt.Println( unsafe.Sizeof( p ) ) // 8
  9. var s []int = nil
  10. fmt.Println( unsafe.Sizeof( s ) ) // 24
  11. var m map[int]bool = nil
  12. fmt.Println( unsafe.Sizeof( m ) ) // 8
  13. var c chan string = nil
  14. fmt.Println( unsafe.Sizeof( c ) ) // 8
  15. var f func() = nil
  16. fmt.Println( unsafe.Sizeof( f ) ) // 8
  17. var i interface{} = nil
  18. fmt.Println( unsafe.Sizeof( i ) ) // 16
  19. }

nil 使用场景

pointers

nil pointer

  • 指向 nil, 又名 nothing
  • pointer 的零值
  1. var p *int // 声明一个 int 类型的指针
  2. println(p) // <nil>
  3. p == nil // true
  4. *p // panic: runtime error: invalid memory address or nil pointer dereference

指针表示指向内存的地址,如果对 nil 的指针进行解引用的话就会导致 panic。那么为 nil 的指针有什么用呢? 先来看看一个计算二叉树和的例子:

  1. type tree struct {
  2. v int
  3. l *tree
  4. r *tree
  5. }
  6. // first solution
  7. func (t *tree) Sum() int {
  8. sum := t.v
  9. if t.l != nil {
  10. sum += t.l.Sum()
  11. }
  12. if t.r != nil {
  13. sum += t.r.Sum()
  14. }
  15. return sum
  16. }

上面代码有两个问题:

  • 一个是代码重复

    1. if v != nil {
    2. v.m()
    3. }

另一个是当 t 是 nil 的时候会 panic:

  1. var t *tree
  2. sum := t.Sum() // panic: invalid memory address or nil pointer dereference

那,怎么解决上面的问题呢? 我们先来看看一个指针接收器的例子:

  1. type Person struct{}
  2. func sayHi(p *Person) {fmt.Println("hi")}
  3. func (p *Person) sayHi() {fmt.Println("hi")}
  4. var p *Person
  5. p.sayHi() // hi

对于指针对象的方法来说,就算指针的值为 nil, 也是可以调用的,基于此,我们可以对刚刚计算的二叉树的例子进行一下改造:

  1. func (t *tree) Sum() int {
  2. if t == nil {
  3. return 0
  4. }
  5. return t.v + t.l.Sum() + t.r.Sum()
  6. }

跟刚才的代码一对比是不是简洁了很多? 对于 nil 指针,只需要在方法前面判断一下就 OK 了,无需重复判断。换成打印二叉树的值或者查找二叉树的某个值都是一样的:Coding Time

  1. func(t *tree) String() string {
  2. if t == nil {
  3. return ""
  4. }
  5. return fmt.Sprint(t.l, t.v, t.r)
  6. }
  7. // nil receivers are useful: Find
  8. func (t *tree) Find(v int) bool {
  9. if t == nil {
  10. return false
  11. }
  12. return t.v == v || t.l.Find(v) || t.r.Find(v)
  13. }

所以如果不是很需要的话,不要用NewX()去初始化值,而是使用它们的默认值。

slices

  1. // nil slices
  2. var s []T
  3. len(s) // 0
  4. cap(s) // 0
  5. for range s {
  6. } // iterates zero times
  7. s[i] // panic: index out of range

一个为nilslice,除了不能索引外,其他的操作都是可以的,slice有三个元素,分别是长度、容量、指向数组的指针,当你需要填充值的时候可以使用append函数,slice会自动进行扩充。所以我们并不需要担心slice的大小,使用append的话slice会自动扩容。

  1. var s []int
  2. for i := 0; i < 10; i++ {
  3. fmt.Printf("len: %2d cap: %2d\n", len(s), cap(s))
  4. s = append(s, i)
  5. }

那么为nil的slice的底层结构是怎样的呢?根据官方的文档,slice有三个元素,分别是长度、容量、指向数组的指针:

深入理解nil - 图1

深入理解nil - 图2

map

对于 Go 来说,map,function, channel 都是特殊的指针,指向各自特定的实现,这个我们暂时可以不用管。

  1. // nil maps
  2. var m map[t]u
  3. len(m) // 0
  4. for range m // interates zero times
  5. v, ok := m[i] // zero(u), false
  6. m[i] = x // panic: assignment to entry in nil map

对于 nilmap, 我们可以简单把它看成是一个只读的 map,不能进行写操作,否则就会 panic。那么,nil 的 map 有什么用呢? 看下这个例子:

  1. func NewGet(url string, headers map[string]string) (*http.Request, error) {
  2. req, er := http.NewRequest(http.MethodGet, url, nil)
  3. if err != nil {
  4. return nil, err
  5. }
  6. for k, v := range headers {
  7. req.Header.Set(k, v)
  8. }
  9. return req, nil
  10. }

对于 NewGet 来说,我们需要传入一个类型为 map 的参数,并且这个函数只是对这个参数进行读取,我们可以传入一个非空的值:

  1. NewGet("http://google.com", map[string]string) {
  2. "USER_AGENT":"golang/gopher",
  3. }
  4. // 或者,这样传
  5. NewGet("http://google.com", map[string]string{})
  6. // 但是,前面也说了,map 的零值是 nil, 所以当 header 为空的时候,我们也可以直接传入一个 nil
  7. NewGet("http://google.com", nil)

是不是,简洁很多? so, 把 nil map 作为一个只读的空的 map 进行读取吧

channels

  1. // nil channels
  2. var c chan t
  3. <- c // blocks forever
  4. c <- x // blocks forever
  5. close(c) // panic: close of nil channel

关闭一个 nil 的 channel 会导致程序 panic (如何关闭 channel 可以看看这篇文章:如何优雅的关闭Go Channel). 举个例子,假如现在有两个 channel 负责输入,一个 channel 负责汇总,简单的代码实现:

  1. func merge(out chan<- int, a, b <-chan int) {
  2. for {
  3. select {
  4. case v := <- a:
  5. out <- v
  6. case v := <- b:
  7. out <- v
  8. }
  9. }
  10. }

closed channels

  1. var c chan t
  2. v, ok <- c // zero(t), false
  3. c <- x // panic: send on closed channel
  4. close(c) // panic: close of nil channel

如果在外部调用中关闭了 a 或者 b, 那么就会不断地从 a 或者 b 中读出 0,这和我们想要的不一样,我们想关闭 a 或 b 后就停止汇总,修改一下代码:

  1. func merge (out chan<- int, a, b <-chan int) {
  2. for a != nil || b != nil {
  3. select {
  4. case v, ok := <-a:
  5. if !ok {
  6. a = nil
  7. fmt.Println("a is nil")
  8. continue
  9. }
  10. out <- v
  11. case v, ok := <-b:
  12. if !ok {
  13. a = nil
  14. fmt.Println("b is nil")
  15. continue
  16. }
  17. out <- v
  18. }
  19. }
  20. fmt.Println("close out")
  21. close(out)
  22. }

在知道 channel 关闭之后,将 channel 的值设为 nil, 这样子就相当于将这个 select case 子句给停用了,因为 nil 的 channel 是永远阻塞的。

functions

函数可以被用作结构体字段, 逻辑上,默认的零值为 nil

  1. type Foo struct {
  2. f func() error
  3. }

nil funcs for default values

lazy initialization of variables, nil can also imply default behavior

  1. func NewServer(logger func(string, ...interface{})) {
  2. if logger == nil {
  3. logger = logger.Printf
  4. }
  5. logger("initializing %s", os.Getenv("hostname"))
  6. // ...
  7. }

interfaces

interface 并不是一个指针,它的底层实现由两部分组成,一个是类型,一个是值,也就类似于:(Type, Value). 只有当类型和值都是 nil 的时候,才等于 nil. 看看下面的代码:

  1. func do() error { // error: (*doError, nil)
  2. var err *doError
  3. return err // nil of type *doError
  4. }
  5. func main() {
  6. err := do()
  7. fmt.Println(err == nil) // false
  8. }

输出结果:false. do 函数声明了一个 doError 的变量 err, 然后返回,返回值是 error 接口,但是这个时候的 Type 已经变成了:(doError, nil), 所以和 nil 肯定是不会相等的。所以我们在写函数的时候,不要声明具体的 error 变量,而是应该直接返回 nil:

  1. func do() error {
  2. return nil
  3. }
  4. // 再来看看这个例子
  5. func do() *doError { // nil of type *doError
  6. return nil
  7. }
  8. func wrapDo() error { // error (*doError, nil)
  9. return do() // nil of type *doError
  10. }
  11. func main() {
  12. err := wrapDo() // error (*doError, nil)
  13. fmt.Println(err == nil) // false
  14. }

这里最终的输出结果也是 false。 为什么呢? 尽管 wrapDo 函数返回的是 error 类型, 但是 do 返回的却是 doError 类型,也就是变成了 (doError, nil), 自然也就和 nil 不相等了。因此,不要返回具体的错误类型。遵从这两条建议,才可以放心的使用 if x != nil.

在Go中,nil只是一个标识符,可用于表示某些类型的零值。它不是单一的价值。相反,它可以表示具有不同

存储器布局的许多值。

links