理解Golang中的 nil

零值

官方语言规范中关于零值的说明:spec#The_zereo_value

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

zero value

什么是nil

根据官方定义,nil是预定义标识,代表了指针pointer通道channel函数func接口interfacemap切片slice类型变量的零值。

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

也就是说,nil仅是下列6中类型的零值:

  • pointer
  • channel
  • func
  • interface
  • map
  • slice

注意:struct类型零值不是nil,而是各字段值为对应类型的零值。且不能将struct类型和nil进行等值判断,语法校验不通过。

如:

1
2
3
4
5
6
7
type T struct { i int; f float64; next *T }
t := new(T) // 此时,var t2 T效果相当,只是t为指针,而t2为类型变量

// 那么存在如下:
t.i == 0
t.f == 0.0
t.next == nil // 指针类型的零值为nil

详细看看各类型nil的情况

nil pointer

  • 指针的零值是nil
  • 只声明、未指向具体地址的指针,此时为nil
  • nil指针依然可以正常调用指针对象的方法
1
2
3
var p *int
p == nil // true
*p // panic: invalid memory address or nil pointer dereference

Golang中指针区别几乎和C/C++中功能相近(都是指向内存地址),区别在于:

  • 无需担心内存泄露,即内存安全
  • GC垃圾回收

nil slice

切片的零值是nil,所以只声明变量时,其缺省值为零值nil,这时也就是我们所说的nil slice
nil切片不能直接访问元素值,但可通过append()追加元素。

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

var a1 []int
fmt.Printf("%#v\n", a1) // []int(nil)
a2 := make([]int, 0)
fmt.Printf("%#v\n", a2) // []int{}
a3 := []int{}
fmt.Printf("%#v\n", a3) // []int{},等价于make效果

nil map

  • 只声明一个map类型变量时,为nil map
  • 此时为只读map,无法进行写操作,否则会触发panic
  • nil mapempty map区别:
    • nil map:只声明未初始化,此时为只读map,不能写入操作,示例:var m map[t]v
    • empty map:空map,已初始化,可写入,示例:m := map[t]v{}m := make(map[string]string, 0)
1
2
3
4
5
6
7
// nil maps
var m map[t]u // nil map
m2 := map[t]u{} // empty map
len(m) // 0
for range m // iterates zero times
v, ok := m[i] // zero(u), false
m[i] = x // panic: assignment to entry in nil map

一个常见的应用场景,NewX返回特定类型对象:

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

// 自定义header
NewGet("http://google.com", map[string]string{
"USER_AGENT": "golang/gopher",
},)

// 无自定义header时,传empty map
NewGet("http://google.com", map[string]string{})
// 无自定义header,也可以传nil map
NewGet("http://google.com", nil)

nil channel

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 func

mapchannelfunction的本质都是指向具体实现的指针,而对应类型的nil则是不指向任何地址。

nil interface

interface底层由两部分组成:类型、值(type, value),当二者均为nil时,此时interface才为nil。

(nil, nil) is nil

1
2
var s fmt.Stringer    // Stringer (nil, nil)
fmt.Println(s == nil) // true

结论1interface (nil, nil) == nil,类型和值均为nil的interface,等于nil

(type, nil) is not nil

1
2
3
4
var p *Person           // nil of type *Person
var s fmt.Stringer = p // Stringer (*Person, nil)
fmt.Printf("%#v\n", s) // 注意,此时打印输出为(*Person)(nil),无任何是interface的体现
fmt.Println(s == nil) // false

不要返回具体的错误类型,而应直接返回nil

下面展示返回类型为interface时的差异:

错误示例:

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() // error(*doError, nil)
fmt.Println(err == nil) // false
}

正确示例:

1
2
3
4
5
6
7
8
func do() *doError {   // nil of type *doError
return nil
}

func main() {
err := do() // nil of type *doError
fmt.Println(err == nil) // true
}

再看下面这段代码,虽然do()返回nil,但wrapDo()返回依然是接口,也就是类型为*doError,值为nil的接口,此时拿到的返回值并不等于nil。

1
2
3
4
5
6
7
8
9
10
11
12
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
}

nil的有效利用

  • nil类型接收者是可以正确调用方法的 nil reveivers are userful
  • Keep nil (pointer) useful if possible, if not NewX()
  • Use nil slices, they're often fast enough
  • Use nil maps as read-only empty maps,将nil map作为只读的空map(不能读nil map进行写入操作,否则会发生panic)
  • Use nil channel to disable a select case,nil channel来阻塞selct/case语句
  • nil value can satisfy interface,不同类型的nil值可满足interface,也就是可赋值给interface
  • Use nil interface to signal defaul,使用nil的interface来标识使用缺省处理
  • nil is an important part Go

nis-useful

nil pointer用法

  • nil pointer用来和nil比较确认是否为零值
1
2
3
var p *int
p == nil // true
*p // panic: runtime error: invalid memory address or nil pointer dereference

来看看,如何实现二叉树树的求和操作:

1
2
3
4
5
6
7
type tree {
v int
l *tree
r *tree
}

func (t *tree) Sum() int

第一种方案,有两个问题:

  • 代码冗余,重复的if v != nil {v.m()}
  • t为nil时,会发生panic
1
2
var t *tree // nil of type *tree
sum := t.Sum() // panic
1
2
3
4
5
6
7
8
9
10
11
// 实现方案1
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
}

nil接收者,也可以正确调用方法,所以可以利用这一点,改造出方案2:

1
2
3
4
5
6
7
// 方案2:代码简洁、可断性提高很多
func (t *tree) Sum() int {
if t == nil {
return 0
}
return v + t.l.Sum() + t.r.Sum()
}

通过利用类型的nil,可以灵活实现是否为空的处理,已经便捷实现扩展函数:

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

func (t *tree) Find(v int) bool {
if t == nil {
return false
}
return t.v == v || t.l.Find(v) || t.r.Find(v)
}

nil slices用法

  • 不能对nil slice进行取值,否则会发生panic
  • 可通过append函数对nil slice进行增加元素操作
1
2
3
4
5
6
7
8
9
10
var s []int
len(s) // 0
cap(s) // 0
for range s // 执行0次
s[i] // panic: index out of range

for i := 0; i <10; i++ {
fmt.Printf("len: %2d, cap: %2d\n", len(s), cap(s))
s = append(s, i)
}

nil map用法

  • nil map不能进行增加元素操作,因它还没有进行初始化
  • nil map作为只读的空map
1
2
3
4
5
var m map[t]u
len(m) // 0
for range m // 执行0次
v, ok := m[i] // v=zero(u), ok=false
m[i] = x // panic: assignment to entry in nil map
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
// 有个nil map有用的例子
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
}

// 调用时,传递headers
NewGet(
"http://google.com",
map[string]string{
"USER_AGENT":"google/gopher",
}, // go语言五十度灰,如果参数和)之间换行形式,参数尾部需追加,逗号
)

// 调用时,不传递headers,可以传递一个empty map空map
NewGet(
"http://google.com",
map[string]string{}, // 传递空map,empty map
)

// 调用时,不传递headers,可以传递一个nil map
NewGet(
"http://google.com",
nil, // 传递nil map,也是合法的
)

nil channel用法

  • 不能对nil channel进行close()操作,发触发panic: close of nil channel
  • 不能对channel进行多次close,会触发 panic: close of closed channel
  • 关闭channel,在select/case中将依然能获取到值,但nil channel将阻塞读操作来失效select/case中逻辑
1
2
3
4
5
6
7
8
9
10
var c chan t // nil of type chan t
// nil channel操作时
<- c // block forever,持续阻塞
c <- x // block forever,持续阻塞
close(c) // panic: close of nil channel,关闭nil channel发生panic

// 对于已关闭的channel,将发生如下现象
v, ok := <-c // zero(t), false 不会阻塞,返回零值和False
c <-x // panic: send to close channel
close(x) // panic: close of closed channel,备注原文中错误

现在假设要实现一个合并函数,实现从两个通道中获取数据,然后写入out输出通道:

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
func merge(out chan<- int, a,b <-chan int)  {
for {
select {
case v:= <-a: // 当a/b通道关闭时,这里将持续获取到0
out <-v
case v:= <-b
out <-v
}
}

}

// 对于a/b通道关闭的情况,改造代码如下:
// 此时不会获取到零值,但是可能会发生panic, deadlock
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 closed")
continue
}
out <-v
case v,ok := <-b:
if !ok {
bClosed = true
fmt.Println("b is closed")
continue
}
out <-v
}
}
close(out) // 需要再不使用后进行close操作
}

终于搞定了,提交代码,转交给测试吧!你会发现CPU莫名燃烧了自我,为什么?因为外部逻辑如果关闭了a/b,此时上述代码中会有空转的逻辑,运行下,看看打印输出:

1
2
3
4
5
6
a is closed
a is closed
a is closed
a is closed
a is closed // 很多次无用的空转逻辑
b is closed // 最后一次,a/b都未空时才结束循环

可能看到这里已经有些蒙了,但要明白,无论是否关闭一个chanel,都可以从中读取到值,而如果不需要对channel取值操作了,那么可以将其改为nil,这样会永远阻塞读,防止再发生读操作;同时应在入口增加非nil判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 closed")
continue
}
out <- v
case v, ok := <-b:
if !ok {
b = nil
fmt.Println("b is closed")
continue
}
out <- v
}
}
close(out) // 需要再不使用后进行close操作
}

nil func用法

  • go中函数是一等公民
  • 函数可以作为struct结构体的字段,缺省值则为nil
1
2
3
4
5
6
7
8
9
10
11
type Foo struct {
f func() error // f is type of func() error
}

// 常见用法,传输函数为nil,增加缺省处理
func NewServer(logger func(string, ...interface{})) {
if logger == nil {
logger = log.Printf
}
logger.Printf("initializng %s", os.Getenv("hostname"))
}

nil interface用法

  • nil interface作为一种信号来使用
  • nil指针,不等于nil接口
1
2
3
if err != nil {
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Summer intface {
Sum() int
}

var t *tree // nil of type *tree
var s Summer = t // nil指针,可以是合法的interface类型的值
// 此时,对接接口类型变量s而言,其类型为*tree,值为nil,也就是说(*tree, nil)行的interface
fmt.Println(t==nil, s.Sum()) // true, 0

type ints []int
func (i *ints) Sum() int {
s := 0
for _, v := range i{
s += v
}
return s
}

var i ints
var s Sumer = i // nil value can satisfy interface
fmt.Println(s==nil, s.Sum()) // true, 0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 通过判断接口为nil,来给定缺省值
func doSum(s Summer) int {
if s == nil {
return 0
}
return s.Sum()
}

var t *tree
doSum(t) // interface的类型和值分别为:(*tree, nil)

var i ints
doSum(i) // (ints, nil)

doSum(nil) // (nil, nil)

http.HandleFunc("localhost:8080", nil) // 传递nil,则使用缺省处理

总结

Kinds of nil

slice的零值nil slice,通过append()函数可以给nil slice进行增加元素操作;而对于map则不然,一个nil map,是不能直接进行复制的,必须进行make操作进行初始化开辟内存空间。

这是有一定混淆的。

参考阅读