理解 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
|
对于 nil
的 map
, 我们可以简单把它看成是一个只读的 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 🙂