理解 nil

  • nil 是什么?
  • nil 在 Go 中又是什么?
  • nil 意味着什么?
  • nil 有用吗?

我们常常会把 nil 拼写成 null, 学过 C 的同学肯定听过这个 null 符号,甚至某些让人痛恨的同学还故意取一个这样的名字…

nil 是什么?

从词源来看

nil

Latin nihil meaning nothing (什么也没有)

null

Latin ne + ullus meaning not any (没有,空)

none

Old English ne + ān meaning not one (一个也没有)

在英语中数字 0 的表示

1
2
3
4
5
6
7
- zero      - cipher
- null      - love
- duck      - nil
- nada      - zilch
- zip       - the letter ‘o’
- naught    - nought
- aught     - ought

nil 在 Go 中又是什么?

nil is (a) zero

先看看 nil 的具体表现

1
2
3
4
// 相信这个错误大家在 Go 中经常碰到
panic: runtime error: invalid memory address or nil pointer dereference

// 恐慌运行时错误无效的内存地址或空指针解引用

Note: nil leads to panic –> panic leads to fear –> fear leads to … 不敢想象…

there are many ways, nil is the Go way

zero values ,他们是什么呢?

在 Go 中,如果你声明了一个变量,但是没有对它进行赋值操作,那么这个变量就会有一个类型的默认零值。 So, 来看看 Go 中每种类型对应的零值吧!

1
2
3
4
5
6
bool    ->  false       pointers    ->  nil
numbers ->  0           slices      ->  nil
string  ->  ""          maps        ->  nil
                        channels    ->  nil
                        functions   ->  nil
                        interfaces  ->  nil

结构体中的零值

1
2
3
4
5
6
7
type Person struct {
    AgeYears    int
    Name        string
    Friend      []Person
}

var p Person    // Person{0, "", nil}

上面,变量 p 只声明但没赋值,所以 p 的所有字段都有对应的零值。

nil 的类型

untyped zero 无类型的零值

1
2
3
4
5
a := false
a := ""
a := 0      // a := int(0)
a := 0.0    // a := float64(0)
a := nil    // use of untyped nil

” Go 文档中说:nil 是一个预定义的标识符,代表指针、通道,函数,接口,字典或切片类型的零值,也就是预定好的一个变量

1
2
type Type int
var nil Type

是不是有点惊讶? nil 并不是 Go 的关键字之一,你甚至可以自己去改变 nil 的值:

1
2
// YOLO
var nil = errors.New(`¯\_(ツ)_/¯`)

这样是完全可以编译通过的,但是最好不要这样子去做

zero values 零值们

首先来看看:Kinds of nil, 即 nil 的类型:

1
2
3
4
5
6
pointers        point to nothing
slices          have no backing array
maps            are not initialized
channels        are not initialized
functions       are not initialized
interfaces      have no value assigned, not even a nil pointer

接下来,就对这些 nil 的类型分别做说明: 即 nil 的用处

nil 有什么用?

在了解了什么是 nil 之后,再来说说 nil 有什么用

pointers

pointers in Go :

  • 表示指向内存中的一个位置
  • 和 C、C++ 相似,但
    • 没有指针运算 -> 内存安全
    • 垃圾回收

nil pointer

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type tree struct {
    v int
    l *tree
    r *tree
}

// first solution
func (t *tree) Sum() int {
    sum := t.v
    if t.l != nil {
        sum += t.l.Sum()
    }
    if t.r != nil {
        sum += t.r.Sum()
    }
    return sum
}

上面代码有两个问题:

  • 一个是代码重复

    1
    2
    3
    
    if v != nil {
        v.m()
    }
    • 另一个是当 t 是 nil 的时候会 panic:
    1
    2
    
    var t *tree
    sum := t.Sum()  // panic: invalid memory address or nil pointer dereference

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

1
2
3
4
5
6
7
type Person struct{}

func sayHi(p *Person) {fmt.Println("hi")}
func (p *Person) sayHi() {fmt.Println("hi")}

var p *Person
p.sayHi()           // hi

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

nil receivers are useful: Sum

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

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

nil receivers are useful: String

1
2
3
4
5
6
func (t *tree) String() string {
    if t == nil {
        return ""
    }
    return fmt.Sprintf(t.l, t.v, t.r)
}

nil receivers are useful: Find

1
2
3
4
5
6
func (t *tree) Find(v int) bool {
    if t == nil {
        return false
    }
    return t.v == v || t.l.Find(v) || t.r.Find(v)
}

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

slices

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

一个为 nil 的 slice, 处了不能索引外,其他的操作都是可以的,当你需要填充值得时候可以使用 append 函数, slices 会自动扩充。

append on nil slices

1
2
3
4
5
var s []int
for i := 0; i < 10; i++ {
    fmt.Printf("len: %2d cap: %2d\n", len(s), cap(s))
    s = append(s, i)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ go run main.go
len: 0 cap: 0 []  → s is nil!
len: 1 cap: 1 [0]   → allocation
len: 2 cap: 2 [0 1]   → reallocation
len: 3 cap: 4 [0 1 2]   → reallocation
len: 4 cap: 4 [0 1 2 3]
len: 5 cap: 8 [0 1 2 3 4]  → reallocation
len: 6 cap: 8 [0 1 2 3 4 5]        
len: 7 cap: 8 [0 1 2 3 4 5 6]               
len: 8 cap: 8 [0 1 2 3 4 5 6 7]
len: 9 cap: 16 [0 1 2 3 4 5 6 7 8]  → reallocation

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

slice 内部:

当有元素的时候:slice := make([]byte, 5)

nil slice: var s []byte

所以:我们无需担心 slice 的大小,使用 append 的话 slice 会自动扩容。(视频中说 slice 自动扩容速度很快,不必担心性能问题,这个值得商榷,在确定 slice 大小的情况下只进行一次内存分配总是好的)

maps

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

1
2
3
4
5
6
7
// nil maps
var m map[t]u

len(m)          // 0
for range m     // interates zero times
v, ok := m[i]   // zero(u), false
m[i] = x        // panic: assignment to entry in nil map

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

using maps

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

    for k, v := range headers {
        req.Header.Set(k, v)
    }
    return req, nil
}

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

1
2
3
4
5
6
7
8
9
NewGet("http://google.com", map[string]string) {
    "USER_AGENT":"golang/gopher",
}

// 或者,这样传
NewGet("http://google.com", map[string]string{})

// 但是,前面也说了,map 的零值是 nil, 所以当 header 为空的时候,我们也可以直接传入一个 nil
NewGet("http://google.com", nil)

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

channels

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

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

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

closed channels

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

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

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

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

functions

nil funcs

Go 语言中:函数是头等公民

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

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

nil funcs for default values

lazy initialization of variables, nil can also imply default behavior

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

interfaces

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

1
2
3
4
5
6
7
8
9
func do() error { // error: (*doError, nil)
    var err *doError
    return err  // nil of type *doError
}

func main() {
    err := do()
    fmt.Println(err == nil) // false
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func do() error {
    return nil
}

// 再来看看这个例子

func do() *doError { // nil of type *doError
    return nil
}

func wrapDo() error { // error (*doError, nil)
    return do() // nil of type *doError
}

func main() {
    err := wrapDo()   // error  (*doError, nil)
    fmt.Println(err == nil) // false
}

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

nil is useful

最后,做个总结吧!😋 nil is useful

1
2
3
4
5
6
pointers    methods can be called on nil receivers
slices      perfectly valid zero values
maps        perfect as read-only values
channels    essential(必要) for some concurrency patterns
functions   needed for completeness
interfaces  the most userd signal in Go (err != nil)

See Also

Thanks to the authors 🙂