0%

Go 语言的 nil 的到底怎么用?

Go 语言中每种类型都自己的默认零值,int 的零值是 0,bool 的零值是 false,string 的零值是 “”,而 nil 作为 pointer、slice、map、channel、function、interface 的默认零值,在 Go 语言中有着非常重要的地位。本文就是对 nil 的使用进行深入的探究。

nil 是无类型的(untype)

1
2
3
a := 1
b := ""
d := nil

如上代码所示,a 的 类型是 int, b 的类型是 string,但是 d 是无类型的。nil 是一个预定义的标识符。

nil is a predeclared identifier representing the zero value for pointer, channel, func, interface, map or slice type.

nil 的含义

类型 nil 的含义
pointer 空指针
slice 底层无指向的数组(ptr=nil, len=0,cap=0)
map 未初始化,空指针
channel 未初始化,空指针
function 未初始化,空指针
interface (type=nil,value=nil)

** 特别要注意,interface 等于 nil 时候的情况,由于 interface 是由两部分组成,只有 type=nil,value=nil,interface 才是真的 nil。**

1
2
3
var p *Person             // nil of type *Person
var s fmt.Stringer = p // Stringer (*Person, nil)
fmt.Println(s == nil) // false

nil 其实很有用

pointer

1
2
3
var p *int
p == nil // true
fmt.Println(*p) // panic,因为是空指针没有对应的值

nil receiver

nil receiver 主要作为一个初始化的默认值,例如:

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

slice

1
2
3
4
5
var s []slice    // s = nil
len(s) // 0
cap(s) // 0
for range s // 0次迭代
s[i] // panic: index out of range

从上可知,slice 为 nil 时,这是一个只读 slice,由于没有元素,所以s[i]会报错,但是其他的操作是不会报错的。

初始化 slice 的时候,可以直接定义成 var s []slice,虽然它是 nil,但是通过 append()可以不断添加元素,不用担心在 apped 的过程中需要重新分配内存,因为这样的操作已经足够快了,对大部分程序应该够用了。

map

1
2
3
4
5
var m map[string]int	  // nil
len(m) // 0
for range m // 0次迭代
v, ok := m[i] // 0, false
m[i] = x // panic: assignment to entry in nil map

从上可知,map 为 nil 时,是一个只读空 map。

channel

1
2
3
4
var c chan int            // nil
<- c // 永远阻塞
c <- 1 // 永远阻塞
close(c) // panic: close of nil channel

从上可知,channel 为 nil 时,向 channel 发送数据或者从 channel 中获取数据都是会永久阻塞(这点很重要),close 空 channel 会导致 panic。

如果要实现一个merge(out chan<- int, a, b, <-chan int)的功能,从 a , b 两个 channel 中读取数据,然后写入到 out 这个 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
}
}
}

调用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
a := make(chan int)
b := make(chan int)
out := make(chan int)
go func() {
for i := 0; i < 5; i++ {
a <- i
}
close(a)
}()

go func() {
for i := 5; i < 10; i++ {
b <- i
}
close(b)
}()
go func() {
merge(out, a, b)
}()
for v := range out {
fmt.Print(v, " ")
}
}

输出结果:

1
0 1 5 2 6 7 3 8 9 4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

此时在完成打印任务之后,还会不断的输出0,这是因为 close 之后的 channel,是可以不断获取零值的。

对代码进行一些修改,增加判断 close 的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func merge(out chan<- int, a, b <-chan int) {
var aClosed, bClosed bool
for !aClosed || !bClosed {
select {
case v, ok := <-a:
if !ok {
aClosed = true
continue
}
out <- v
case v, ok := <-b:
if !ok {
bClosed = true
continue
}
out <- v
}
}
}

输出结果:

1
0 1 5 2 6 3 7 4 8 9 fatal error: all goroutines are asleep - deadlock!

因为我们没有关闭 out 通道,导致死锁,因此增加关闭操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func merge(out chan<- int, a, b <-chan int) {
var aClosed, bClosed bool
for !aClosed || !bClosed {
select {
case v, ok := <-a:
if !ok {
aClosed = true
continue
}
out <- v
case v, ok := <-b:
if !ok {
bClosed = true
continue
}
out <- v
}
}
close(out)
}

输出结果:

1
0 1 5 2 6 3 7 4 8 9

结果看起来是我们想要的,但如果我们增加 b 通道的循环写入次数,会发现一个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
"fmt"
)

func main() {
a := make(chan int)
b := make(chan int)
out := make(chan int)
go func() {
for i := 0; i < 5; i++ {
a <- i
}
close(a)
}()

go func() {
for i := 5; i < 1000; i++ {
b <- i
}
close(b)
}()
go func() {
merge(out, a, b)
}()
for v := range out {
fmt.Print(v, " ")
}
}

func merge(out chan<- int, a, b <-chan int) {
var aClosed, bClosed bool
for !aClosed || !bClosed {
select {
case v, ok := <-a:
if !ok {
aClosed = true
fmt.Println("a is now closed")
continue
}
out <- v
case v, ok := <-b:
if !ok {
bClosed = true
continue
}
out <- v
}
}
close(out)
}

输出结果:

1
2
3
4
5
6
7
8
9
10
..........
891 892 a is now closed
a is now closed
a is now closed
893 894 a is now closed
a is now closed
a is now closed
a is now closed
a is now closed
........

从以上结果可知,a 虽然被 close 了,但是下面逻辑一直在被重复,导致 CPU 空转浪费。

1
2
3
4
5
6
7
select {
case v, ok := <-a:
if !ok {
aClosed = true
fmt.Println("a is now closed")
continue
}

此时可以回到我们上面说的,当 channel 为 nil 时,会永久阻塞,因此我们对代码进行以下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 now closed")
continue
}
out <- v
case v, ok := <-b:
if !ok {
b = nil
continue
}
out <- v
}
}
close(out)
}

修改之后,就可以获得想要的结果,因此在日常开发中,可以利用 nil chan 让 select 的一个 case 阻塞

func

1
2
3
4
5
6
func NewServer(logger func(string, ...interface{})) {
if logger == nil {
logger = log.Printf
}
......
}

nil 可以作为函数的默认零值,在初始化的时候进行判断,赋予默认行为。

interface

  • use nil interfaces to signal default
  • nil values can satisfy interfaces