定义

nil是标识符

nil 为预声明的标示符,定义在builtin/builtin.go,

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

// Type is here for the purposes of documentation only. It is a stand-in
// for any Go type, but represents the same type for any given function
// invocation.
type Type int

这是一个预定义的标识符,代表指针、通道、函数、接口、映射或切片的零值,换句话说就是预定义好的一个变量。

nil不是关键字

如果关注过golang关键字的同学就会发现,里面并没有nil,也就是说nil并不是关键字,那么就可以在代码中定义nil,那么nil就会被隐藏。

1
2
3
4
5
6
7
8
9
package main

import "fmt"

func main() {
    nil := 123
    fmt.Println(nil) // 123
    var _ map[string]int = nil //cannot use nil (type int) as type map[string]int in assignment
}

nil没有默认类型

一般预声明标识符都会有一个默认类型,比如Go语言中的itoa默认类型就是int,那么nil的默认类型呢?我们写个例子来看一下:

1
2
3
4
5
6
func main()  {
 const val1 = iota
 fmt.Printf("%T\n",val1)
 var val2 = nil
 fmt.Printf("%T\n",val2)
}
1
2
3
// 运行结果
# command-line-arguments
./nil.go:10:6: use of untyped nil

在编译时就已经报错,编译器告诉我们使用了无类型的nil,所以我们可以得出结论:

nil是没有默认类型的,它的类型具有不确定性,我们在使用它时必须要提供足够的信息能够让编译器推断nil期望的类型。

作用

按照Go语言规范,任何类型在未初始化时都对应一个零值:布尔类型是false,整型是0,字符串是"",而指针、函数、interface、slice、channel和map的零值都是nil。

普通的 struct(非指针类型)的对象不能赋值为 nil,也不能和 nil 进行判等(==),即如下代码,不能判断 *s == nil(编译错误),也不能写:var s Student = nil。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  s := new(Student)  //使用new创建一个 *Student 类型对象
	fmt.Println("s == nil", s == nil) //false
	//fmt.Println(*s == nil) //编译错误:cannot convert nil to type Student
	fmt.Printf("%T\n", s)  //*test.Student
	fmt.Printf("%T\n", *s) //test.Student<pre name="code" class="plain">

type Student struct{}

func (s *Student) speak() {
	fmt.Println("I am a student.")
}

type IStudent interface {
	speak()
}

但是struct的指针对象可以赋值为 nil 或与 nil 进行判等。不过即使 *Student 类型的s3 == nil,依然可以输出s3的类型:*Student

1
2
3
4
//var s3 Student = nil //编译错误:cannot use nil as type Student in assignment
	var s3 *Student = nil
	fmt.Println("s3 == nil", s3 == nil) //true
	fmt.Printf("%T\n", s3)              //*test.Student

nil没有默认的类型,尽管它是多个类型的零值,必须显式或隐式指定每个nil用法的明确类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

func main() {

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

    // 隐式.
    var _ *struct{} = nil
    var _ []int = nil
    var _ map[int]bool = nil
    var _ chan string = nil
    var _ func() = nil
    var _ interface{} = nil
}

nil类型的地址和值的内存占用

nil类型的所有值的内存布局始终相同,换一句话说就是:不同类型nil的内存地址是一样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main
import (
    "fmt"
)
func main() {
    var m map[int]string
    var ptr *int
    var sl []int
    fmt.Printf("%p\n", m)       //0x0
    fmt.Printf("%p\n", ptr )    //0x0
    fmt.Printf("%p\n", sl )     //0x0
}

业务中一般将nil值表示为异常。nil值的大小始终与其类型与nil值相同的non-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
package main

import (
    "fmt"
    "unsafe"
)

func main() {
    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
}

大小是编译器和体系结构所依赖的。以上打印结果为64位体系结构和正式 Go 编译器。对于32位体系结构, 打印的大小将是一半。

对于正式 Go 编译器, 同一种类的不同类型的两个nil值的大小始终相同。例如, 两个不同的切片类型 ( []int[]string) 的两个nil值始终相同。

nil值比较

nil标识符是不能比较的

1
2
3
4
5
6
7
8
9
package main

import (
    "fmt"
)

func main() {
    fmt.Println(nil==nil)
}

输出:

1
tmp/sandbox318449491/main.go:8: invalid operation: nil == nil (operator == not defined on nil)

从 go 的输出结果不难看出,== 对于 nil 来说是一种未定义的操作。

不同类型的nil是不能比较的

1
2
3
4
5
6
7
8
9
package main
import (
    "fmt"
)
func main() {
    var m map[int]string
    var ptr *int
    fmt.Printf(m == ptr) //invalid operation: m == ptr (mismatched types map[int]string and *int)
}

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

  • 两个值中的一个的类型是另一个的基础类型。
  • 两个值之一的类型实现另一个值的类型(必须是interface类型)。
  • 两个值中的一个的类型是定向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.

同一类型的两个nil值可能无法比较

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func main()  {
 // 指针类型的nil比较
 fmt.Println((*int64)(nil) == (*int64)(nil))
 // channel 类型的nil比较
 fmt.Println((chan int)(nil) == (chan int)(nil))
 // func类型的nil比较
 fmt.Println((func())(nil) == (func())(nil)) // func() 只能与nil进行比较
 // interface类型的nil比较
 fmt.Println((interface{})(nil) == (interface{})(nil))
 // map类型的nil比较
 fmt.Println((map[string]int)(nil) == (map[string]int)(nil)) // map 只能与nil进行比较
 // slice类型的nil比较
 fmt.Println(([]int)(nil) == ([]int)(nil)) // slice 只能与nil进行比较
}

运行结果:

1
2
3
4
# command-line-arguments
./nil.go:13:28: invalid operation: (func())(nil) == (func())(nil) (func can only be compared to nil)
./nil.go:17:36: invalid operation: (map[string]int)(nil) == (map[string]int)(nil) (map can only be compared to nil)
./nil.go:19:27: invalid operation: ([]int)(nil) == ([]int)(nil) (slice can only be compared to nil)

nil的使用

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

pointers

众所周知,方法(姑且这样说)和变量是存储在不同区域的,当我们用一个空指针类型的变量(如,var e *AddrError)调用方法时,该方法是会执行的,只有在执行该空指针变量的解指针操作时,才会 panic。

下面是一个例子:

 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
// Package main provides ...
package main

import "fmt"

func main() {
    var e *Err
    e.Print()
    e.Printf()
    e.Println()
}

type Err struct {
    err string
}

func (e *Err) Print() {
    fmt.Println("e.Print run")
}

func (e *Err) Printf() {
    fmt.Println(e.err)
}

func (e Err) Println() {
    fmt.Println("e.Println run")
}
  1. e.Print() 是完全可以执行的,没有解指针操作。

  2. e.Printf() 在调用 e.err 的时候,发生了解指针操作,故会 panic。

  3. e.Println() 由于接受者是 Err 而不是 *Err,Golang 内部在调用该函数时,会自动解指针,故会 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
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

一个为nil的slice,除了不能索引外,其他的操作都是可以的,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

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

与nil比较

只有声明了但没赋值的interface才是nil interface,只要赋值了,即使赋了一个nil类型,也不是nil 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
}

接口对象的指针可以赋值为nil 或者与 nil 判等(==)

1
2
3
s2 := new(interface{})
	fmt.Println("s2 == nil", s2 == nil)   //false
	fmt.Println("*s2 == nil", *s2 == nil) //true

method

nil interface是不能调用方法的,如果调用方法,比如xxx是个nil interface,那么xxx.M()会runtime error。

另外,如果interface变量的值是空的(比如空struct),那么这个interface的值类型就是<nil>,这时候.method()来调用方法的话,传递进去的receiver的值类型也将是nil

注意,这种情况下,如果method里再调用,就会抛出运行时异常

举例:

 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
package main

import "fmt"

type I interface {
    M()
}

type T struct {
    S string
}

func (t *T) M() {
    if t == nil {
        fmt.Println("<nil>")
        return
    }
    fmt.Println(t.S)
}

func main() {
    var i I

    var t *T
    i = t
    describe(i)
    i.M()

    i = &T{"hello"}
    describe(i)
    i.M()
}

func describe(i I) {
    fmt.Printf("(%v, %T)\n", i, i)
}

输出

1
2
3
4
(<nil>, *main.T)
<nil>
(&{hello}, *main.T)
hello

如果把上面M()里面的return这行注释掉,则会输出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
(<nil>, *main.T)
<nil>
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xc8971]

goroutine 1 [running]:
main.(*T).M(0x0, 0x0)
    /tmp/sandbox104196984/main.go:18 +0x31
main.main()
    /tmp/sandbox104196984/main.go:27 +0x60

当声明一个interface时候,这个interface是nil,然后给这个interface赋了一个为nil的变量时候,这个interface就不是nil。

常见问题

函数返回

1
2
3
4
func nilReturn() (string,error)  {

    return nil,nil  //cannot use nil as type string in return argument
}

因为error是接口类型所以error类型没有报错。

map的nil

key map的key为指针、函数、interface、slice、channel和map,则key可以为nil。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main
import (
    "fmt"
)
func main() {
    mmap := make(map[*string]int,4)
    a:="a"
    mmap[&a] = 1
    mmap[nil] = 99
    fmt.Println(mmap)   //map[0xc042008220:1 <nil>:99]
}

参考

https://blog.keyboardman.me/2018/05/12/nil-in-go/

面试官:两个nil比较结果是什么?