golang中你应该知道的nil知识

photo by Kalen Emsley on Unsplash

Wisdom has never made a bigot, but learning has.
— Josh Billings

1.1 什么是 nil

解释在 Go 语言中 nil 的含义,以及在实际编程中可能出现的使用场景。

1
2
3
// nil is a predeclared identifier representing the zero value for a  
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type

在 Go 语言中,nil 是预定义的标识符,它是许多类型(如指针(pointer)、通道(channel)、函数(func)、接口(interface)、map、切片(slice) 等)的零值。如果我们声明但不初始化一个指针变量,那么这个指针的值就是 nil,如下:

1
2
3
4
5
var p *int
fmt.Println(p)

// run
<nil>

它的两个重要应用场景:

  1. 表示一个未初始化的对象:如上所述,在 Go 语言中,许多类型的零值都是 nil。
  2. 在条件语句中检查错误:用 nil 做判断是一种检查错误的常见方法。(但实际上应视为语言的重要部分,读完本文你应该能理解为什么这么说)
1
2
3
4
file, err := os.Open("file.txt")
if err != nil {
// handle error
}

1.2 nil 的一些特点

探讨 nil 的一些特点

预声明标识符nil没有默认类型

首先了解一下标识符的定义:

Identifiers: Identifiers name program entities such as variables and types. An identifier is a sequence of one or more letters and digits. The first character in an identifier must be a letter. from: The Go Programming Language Specification - The Go Programming Language

Go中其它的预声明标识符都有各自的默认类型,比如

  • 预声明标识符truefalse的默认类型均为内置类型bool
  • 预声明标识符iota的默认类型为内置类型int

nil是没有默认类型的,它的类型具有不确定性。 我们必须在代码中提供足够的信息以便让编译器能够推断出nil值的期望类型。
通过代码我们可以清晰地理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func main() {
p := nil
fmt.Println(p)
}
// run
use of untyped nil in assignment
 
func main() {
var a = (*struct{})(nil)
var b = []int(nil)
var c = map[int]bool(nil)
var d = chan string(nil)
var e = (func())(nil)
var f = interface{}(nil)
fmt.Println(a, b, c, d, e, f)
}
// run
<nil> [] map[] <nil> <nil> <nil>

nil不是一个关键字

首先了解一下关键字的定义:

 keywords: keywords are reserved and may not be used as identifiers. from The Go Programming Language Specification - The Go Programming Language

仍旧通过一个例子来理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// case 1
func main() {
nil := 345
fmt.Println(nil)
true := 567
fmt.Println(true)
}
// run
345
567

// case 2
func main() {
nil := 345
fmt.Println(nil) // 345

var slice []string = nil
fmt.Println(slice)
}
// run
cannot use nil (variable of type int) as []string value in variable declaration

从例子可以确定nilGo语言中并不是关键字,我们可以定义变量名为nil变量(不过不建议这么做)。

当然我们也可以从官方文档中进一步佐证:

golang 中的预保留标识符(Predeclared identifiers):The Go Programming Language Specification - The Go Programming Language
golang 中的关键字列表:The Go Programming Language Specification - The Go Programming Language

1.3 nil 的比较

nil 在比较运算符中的表现,以及如何正确地比较 nil。

同类型的 nil 比较

  1. 预声明标识符nil没有默认类型时,可以与任何类型的变量进行比较。(在我们处理err != nil 时,能够明显感受到。 :P)
1
2
3
4
5
6
7
8
var p *int
fmt.Println(p == nil)

var q *int32
fmt.Println(q == nil)
// run
true
true
  1. 编译器能够推断出nil值的期望类型时,部分可以比较,但部分不可以。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// case 1
func main() {
fmt.Println((*int64)(nil) == (*int64)(nil))
fmt.Println((chan int)(nil) == (chan int)(nil))
fmt.Println((interface{})(nil) == (interface{})(nil))
}
// run
true
true
true

// case 2
func main() {
fmt.Println((func())(nil) == (func())(nil))
fmt.Println((map[string]int)(nil) == (map[string]int)(nil))
fmt.Println(([]int)(nil) == ([]int)(nil))
}
// run
./prog.go:8:14: invalid operation: (func())(nil) == (func())(nil) (func can only be compared to nil)
./prog.go:9:14: invalid operation: (map[string]int)(nil) == (map[string]int)(nil) (map can only be compared to nil)
./prog.go:10:14: invalid operation: ([]int)(nil) == ([]int)(nil) (slice can only be compared to nil)

指针类型nilchannel类型的nilinterface类型可以相互比较,而func类型、map类型、slice类型只能与nil标识符比较,两个类型相互比较是不合法的。

The Go Programming Language Specification - The Go Programming Language 中提到了 Slice, map, and function types are not comparable. 但这是为什么呢?

原因主要包含以下几点:

  • Slices: Slices are dynamic, ordered collections of elements. Their equality depends on the equality of all their elements, in the same order. Since elements themselves can be slices, maps, or functions, the comparison process could become infinitely recursive, leading to potential stack overflows or unpredictable behavior.

  • Maps: Maps are unordered collections that associate unique keys with values. As keys themselves, maps would introduce circular references during comparison. Imagine comparing two maps, where one map’s key might reference the other map, creating an endless loop.

  • Functions: Functions are essentially blocks of code that can be assigned to variables or passed around. Their equality is a complex concept. Should function equality depend solely on the function’s name, or should it delve into the function’s body and analyze its logic for exact duplication? This ambiguity makes defining a clear comparison for functions impractical.

Golang’s decision to restrict the comparability of slices, maps, and functions promotes efficient map operations and avoids potential issues with circular references or ambiguous function comparisons.

不同类型的 nil 比较

不同类型的 nil 之间是一般不能进行比较的,即便它们的内存表示都是零,通常试图这样做在编译时就会报错。

首要原因,我觉得是不同类型的内存布局一般都是不一样的。举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
var p *struct{} = nil
fmt.Println(unsafe.Sizeof(p)) // 8

var s []int = nil
fmt.Println(unsafe.Sizeof(s)) // 24

var p *int
var q *float64
fmt.Println(p == q)
// run
invalid operation: p == q (mismatched types *int and *float64)

另外就是 Golang 自身的一些约束条件,使得比较不合法。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
var ptr *int64 = nil
var cha chan int64 = nil
var ma map[string]string = nil
var slice []int64 = nil
var inter interface{} = nil

fmt.Println(cha == ptr)
fmt.Println(inter == ma)
fmt.Println(inter == slice)
}
// run
./prog.go:14:21: invalid operation: cha == ptr (mismatched types chan int64 and *int64)
./prog.go:15:23: invalid operation: inter == ma (map can only be compared to nil)
./prog.go:16:23: invalid operation: inter == slice (slice can only be compared to nil))

这里的约束条件返回结果已经给出来了,slice or map can only be compared to nil。 至于为什么会有这样的约束条件,已经在上文中给出。

接下来进行到我们的重点,不同类型的 nil 却可以相互比较的例子:

1
2
3
4
5
6
7
8
9
10
11
func main() {
var ptr *int64 = nil
var cha chan int64 = nil
var inter interface{} = nil

fmt.Println(ptr == inter)
fmt.Println(cha == inter)
}
// run
false
false

通过例子我们可以观察到,指针类型、channel类型的 nil 可以和接口类型的 nil 比较。为什么呢?
首先我们需要先了解一下Golang中的接口的源码定义。接口类型的变量(不管是iface还是eface)包含了两个部分:类型和值。

1
2
3
4
5
6
7
8
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}

iface 和 eface 都是 Go 中描述接口的底层结构体,区别在于 iface 描述的接口包含方法,而 eface 则是不包含任何方法的空接口:interface{}

它可以接受任何类型的值。当接口类型的变量未被赋值(即它的类型和值都是 nil),或者它的值被显式置为 nil 时,我们说该接口的值为 nil。

当你尝试比较 fmt.Println(ptr == inter),Go 语言会将 ptr 的类型(*int64)和值(nil)与 inter 的类型和值(都为 nil)进行比较。

尽管 ptr 和 inter 的值都为 nil,但由于它们的类型并不相同,因此比较的结果为 false。机制上来说,Go 这样处理是为了保持类型安全,避免可能出现的意外情况。

1.4 nil 使用过程中的注意点

由上我们可以有一个初步的结论:在 Go 语言中,nil 不仅代表零或无,更重要的是它在许多上下文中都有着重要的含义和用途。
下面我们来看一下Golang中各种类型使用nil的一些注意点。

接口与nil

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
import (
"fmt"
)

type animal interface {
eat()
}

type dog struct{}

func (d *dog) eat() {
fmt.Println("dog is eating bones")
}

func GetNilConcreteAnimal() animal {
var dog *dog
return dog
}

func main() {
nilConcreteAnimal := GetNilConcreteAnimal()
fmt.Println(nilConcreteAnimal == nil)
}

// run
false

输出结果是false,如果你认为是 true,那么你就忽略了接口类型的一个重要概念,interface 不是单纯的值,而是分为类型和值。所以必须要类型和值同时都为 nil 的情况下,interface 的 nil 判断才会为 true

函数与 nil

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
import (
"fmt"
)

type animal interface {
eat()
}

type dog struct {
name string
}

func (d *dog) eat() {
fmt.Println("dog is eating bones")
fmt.Printf("dog %s is eating bones\n", d.name)
}

func GetNilConcreteAnimal() animal {
var dog *dog
return dog
}

func GetConcreteAnimal() animal {
dog := &dog{}
return dog
}

func main() {
concreteAnimal := GetConcreteAnimal()
concreteAnimal.eat()

nilConcreteAnimal := GetNilConcreteAnimal()
nilConcreteAnimal.eat()
}

// run
dog is eating bones
dog is eating bones
dog is eating bones
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x481e20]

goroutine 1 [running]:
main.(*dog).eat(0x0)
/tmp/sandbox2244011392/prog.go:17 +0x60
main.main()
/tmp/sandbox2244011392/prog.go:35 +0x2c

从输出结果可以得到一个要点:

  1. 空指针的实例,仍然可以访问其方法,但是你不能去访问这个空指针(初期实际使用中很容易写出这种低级错误)

切片,映射与 nil

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
func main() {
var slice []int64 = nil
fmt.Println(len(slice))
fmt.Println(cap(slice))
for range slice {

}
fmt.Println(slice[0])
}
// run
0
0
panic: runtime error: index out of range [0] with length 0

goroutine 1 [running]:

func main() {
var m map[string]string
fmt.Println(len(m))
fmt.Println(m["tfrain"])
m["tfrain"] = "wesleywei"
}

// run
0

panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
  1. 一个为nil的slice,进行访问会引发panic
  2. 一个nilmap可以读数据,但是不可以写入数据,否则会发生panic

或许,如果你想偷个懒,都可以简单理解“不要访问空指针”。

通道与 nil

1
2
3
4
5
6
7
8
9
func main() {
var cha chan int
close(cha)
}
// run
panic: close of nil channel

goroutine 1 [running]:
main.main()

1.关闭nilchannel会引发panic

1.5 更多

建议看一下这个视频,希望你会对nil有一个新的理解。

GopherCon 2016: Francesc Campoy - Understanding nil - YouTube

由 AI 生成的总结如下:

这个视频是对Go语言中”null”概念的深度讨论。主讲人深入讲解了在Go语言编程中,如何理解和使用”null”,并且强调了”null”在编程中的重要性。以下是一些视频的主要观点:

  1. “null”在Go中被普遍看作错误情况,但实际上应视为语言的重要部分。
  2. “null”在Go语言的基本结构中无处不在,比如切片(slices),映射(maps),通道(channels) 和函数(functions)。
  3. “null”接口可以表示默认行为,比如HTTP包的默认操作。
  4. “null”接口也可以表示一个没有错误的条件。
  5. “null”在并发编程中具有必要性。
  6. 希望听众停止将”null”视为错误条件,而是理解并接纳它作为Go语言的重要部分。

总的来说,视频主张认可并理解”null”的重要性,了解它在Go语言中的使用方式,并认识到避免使用”null”可能并不是最好的策略。

1.6 参考

Go中的nil -Go语言101
GopherCon 2016: Francesc Campoy - Understanding nil - YouTube

更多该系列文章,参考medium链接:
https://wesley-wei.medium.com/list/you-should-know-in-golang-e9491363cd9a

English post: https://programmerscareer.com/golang-nil/
作者:Wesley Wei – Twitter Wesley Wei – Medium
注意:本文为作者原创,转载请注明出处。

golang中你应该知道的context 知识 golang中你应该知道的flag库知识

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×