go中的nil

什么是nil

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

nil是预先声明的标识符,你可以使用nil而不用声明它。注意nil不是关键字,可以作为变量名并去改变nil的值。

1
2
3
4
func main() {
nil := 1
fmt.Println(nil) // 1
}

默认零值与默认类型

在Go语言中,如果你声明了一个变量但是没有对它进行赋值操作,那么这个变量就会有一个类型的默认零值。

1
2
3
var _ bool   // false
var _ int // 0
var _ string // ""

nil可以代表许多类型的零值。在Go中,nil可以表示(pointer(including type-unsafe),map,slice,function,channel,interface)类型的零值,*new(T)也等于nil。换句话说,在Go中,nil可能是许多不同类型的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
var _ *struct{}    // nil
var _ []int // nil
var _ map[int]bool // nil
var _ chan string // nil
var _ func() // nil
var _ interface{} // nil

_ = *new(*int) // nil
_ = *new([]int) // nil
_ = *new(map[int]bool) // nil
_ = *new(chan string) // nil
_ = *new(func()) // nil
_ = *new(interface{}) // nil

Go中的每个其他预定义标识符都有一个默认类型。但是nil没有默认类型,尽管它有很多可能的类型。编译器必须有足够的信息来从上下文中推导出nil的类型。

1
2
3
4
5
6
7
8
9
_ = 1    // int
_ = true // bool

_ = (*struct{})(nil)
_ = []int(nil)
_ = map[int]bool(nil)
_ = chan string(nil)
_ = (func())(nil)
_ = interface{}(nil)

nil的大小

一个类型的所有值的内存布局总是相同的。类型的nil值不是例外。nil的大小总是与类型相同的非零值的大小相同。因此,表示不同类型的nil可能具有不同的大小(大小是编译器和架构相关的,以下是64位机器和标准Go编译器的结果)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

nil的对比

两个不同类型的nil可能不可对比。在Go中,只有两个不同的可比较类型的两个值只能在其中一个可以隐式转换为另一个类型时才能进行比较。具体而言,有三种情况可以比较两种不同可比较的两种值,nil也遵循同样的规则。

  1. 两个值中的一个的类型是另一个的基础类型。
  2. 两个值之一的类型实现另一个值的类型(必须是interface类型)。
  3. 两个值中的一个的类型是定向channel类型,另一个是双向channel类型,且具有相同的元素类型,并且两种类型中的一种不是定义的类型。

注意如果比较的两个nil之一是一个interface,另一个不是,那么比较结果总是false。原因是在进行比较之前,非interface值将被转换为interface值的类型。转换的interface值具有一个具体的动态类型,但另一个interface值没有。这就是为什么比较结果总是false的原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type IntPtr *int
// IntPtr的基础类型是* int
var _ = IntPtr(nil) == (*int)(nil)

// Go中的每种类型都实现了interface {}类型
var _ = (interface{})(nil) == (*int)(nil) // false

// 定向channel类型的值可以转换为具有相同元素类型的双向channel类型
var _ = (chan int)(nil) == (chan<- int)(nil)
var _ = (chan int)(nil) == (<-chan int)(nil)

// 下面这些无法编译通过,无法隐式转换
var _ = (*int)(nil) == (*bool)(nil) // error: mismatched types *int and *bool.
var _ = (chan int)(nil) == (chan bool)(nil) // error: mismatched types chan int and chan bool.

在Go中,mapslicefunction类型不支持比较。因此,比较使用任何类型的不可比类型指定的两个nil是非法的。但是这些类型的任何值都可以与nil标识符进行比较。

1
2
3
4
5
6
7
8
9
// 下面这些无法编译通过
var _ = ([]int)(nil) == ([]int)(nil) // error: slice can only be compared to nil.
var _ = (map[string]int)(nil) == (map[string]int)(nil) // error: map can only be compared to nil.
var _ = (func())(nil) == (func())(nil) // error: func can only be compared to nil.

// 以下几行可以编译
var _ = ([]int)(nil) == nil
var _ = (map[string]int)(nil) == nil
var _ = (func())(nil) == nil

nil的使用

在了解了什么是nil之后,再来说说nil的使用场景。

pointers

指针表示指向内存的地址,如果对为nil的指针进行解引用的话就会导致panic

1
2
3
var p *int
p == nil // true
*p // panic: invalid memory address or nil pointer dereference

那么为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
type person struct {}
func sayHi(p *person) { fmt.Println("hi") }
func (p *person) sayHi() { fmt.Println("hi") }
var p *person
p.sayHi() // hi

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

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了,无需重复判断。换成打印二叉树的值或者查找二叉树的某个值都是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func(t *tree) String() string {
if t == nil {
return ""
}
return fmt.Sprint(t.l, t.v, t.r)
}

// nil receivers are useful: Find
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
7
// nil slices
var s []T
len(s) // 0
cap(s) // 0
for range s {
} // iterates zero times
s[i] // panic: index out of range

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

map

1
2
3
4
5
6
7
// nil maps
var m map[T1]T2
len(m) // 0
for range m {
} // 迭代0次
v, ok := m[t1] // T2的零值, false
m[t1] = x // panic: assignment to entry in nil map

对于nilmap,我们可以简单把它看成是一个只读的map,不能进行写操作,否则就会panic

1
2
3
4
5
6
7
8
9
10
11
func NewGet(url string, headers map[string]string) (*http.Request, error) {
req, err := 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)

channel

1
2
3
4
5
6
7
8
// nil channels
var ch chan struct{}

// 发送和接受都会永久阻塞
<-ch
ch <- struct{}{}

close(ch) // panic: runtime error: close of nil channel

这可能起初看起来并不是很有用,如果在使用之前忘记使用make初始化channel。然而,这个属性可以用一些聪明的方式来利用,特别是当你需要在select语句中动态地禁用一个case时。举个例子,假如现在有两个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
}
}
}

如果在外部调用中关闭了a或者b,那么就会不断地从a或者b中读出0,因为从一个关闭的channel接收会得到channel类型的零值。在我们的例子中, 类型是int, 所以值是 0,这和我们想要的不一样,我们想关闭a和b后就停止汇总了。

首先使用v, ok语法。当使用这个语法时,ok是一个布尔值,channel是开着的,它为true。这样可以避免将多余的零值发送给out

1
v, ok := <- c

然后正如开头提到的那样, 从nil channels接收数据会永远阻塞。所以在知道channel关闭后,为了禁用一个从channel接收数据的case, 我们可以简单将 channel设置为nil 。修改一下代码。

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 {
b = nil
fmt.Println("b is nil")
continue
}
out <- v
}
}
fmt.Println("close out")
close(out)
}

interface

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函数声明了一个*doErro的变量err,然后返回,返回值是error``interface,但是这个时候的Type已经变成了:(*doError,nil),所以和nil肯定是不会相等的。所以我们在写函数的时候,不要声明具体的error变量,而是应该直接返回nil

1
2
3
func do() error {
return nil
}