Golang nil 小知识

1,568 阅读6分钟

一. 与nil相遇

1.1 nil?

日常开发中,我们经常使用nil来判断某种类型是否为空,nil的作用单单只是这个吗?我们是否踩过nil的坑?nil到底是什么?让我们一起来探索nil的奥秘。读完这篇文章,让大家理解nil,用好nil。

1.2 nil简介

在Go语言中,nil是预定义标识,可以在Go语言标准文档builtin/builtin.go标准库中找到nil的定义。nil代表了指针pointer、通道channel、函数func、接口interface、map、切片slice类型变量的零值,源码如下。

// 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

// 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不是关键字,那么就可以在代码中更改nil,标准的nil就会被隐藏!(不建议)。

func main() {
   var nil = "不建议更改nil"
}

二. 各类型为nil时的地址和大小

2.1 各类型为nil时的地址

func main() {
    var p *int = nil
    var c chan int = nil
    var f func() = nil
    var m map[int]int = nil
    var s []int = nil
    var i interface{} = nil
    fmt.Printf("%p\n", p) // 0x0
    fmt.Printf("%p\n", c) // 0x0
    fmt.Printf("%p\n", f) // 0x0
    fmt.Printf("%p\n", m) // 0x0
    fmt.Printf("%p\n", s) // 0x0
    fmt.Printf("%p\n", i) // %!p(<nil>)
}

从代码中可以直观的看到指针、管道、函数、map、切片slice为nil时输出的地址都为0x0,可以验证不同类型nil值地址都是相同的。而其中比较特殊的是接口,输出的是%!p(<nil>),大致的原因是因为nil的接口经由reflect.ValueOf()函数输出的类型为<invalid reflect.Value>,针对于这种类型Printf函数进行了特别的拼接最终得到%!p(<nil>),感兴趣的同学可以参考一下标准库中fmt/print.go中的Printf函数的实现,这里就不喧宾夺主了。

2.2 各类型为nil时的大小

还是直接上代码,本台机器操作系统是64位的。

func main() {
    var p *int = nil
    fmt.Println("int: ", unsafe.Sizeof(p))
    var c chan int = nil
    fmt.Println("chan int: ", unsafe.Sizeof(c))
    var f func() = nil
    fmt.Println("func: ", unsafe.Sizeof(f))
    var m map[int]int = nil
    fmt.Println("map: ", unsafe.Sizeof(m))
    var s []int = nil
    fmt.Println("slice: ", unsafe.Sizeof(s))
    var i interface{} = nil
    fmt.Println("interface: ", unsafe.Sizeof(i))
}

// 输出
int: 8
chan int: 8
func: 8
map: 8
slice: 24
interface: 16

由上述输出可以看到各类型为nil时的大小有所差异,这里以map、slice作为两个特例来进行说明。map定义时编译器返回的是指针类型,在64位操作系统上,指针类型会分配8个字节大小的空间。slice输出是24,让我们来看一下slice底层的数据结构。

源码之前了无秘密

// slice底层数据结构

type slice struct {

    array unsafe.Pointer //指向底层数组的指针

    len int //切片的长度

    cap int //切片的容量

}

可以从slice底层数据结构构成分析出该结构体所占大小为24(64位操作系统结果是24,32位操作系统结果是12),感兴趣可以验证一下。

三. 不同类型与nil的比较

3.1 nil

可以理解为两个预定义标识符在进行比较,会报错,报错信息如下。但如果对nil进行重定义,标准的nil就会被覆盖,更改后的nil可以进行比较。

func main() {
   // var nil = "不建议更改nil"
   fmt.Println(nil == nil)
}

// 报错
// invalid operation: nil == nil (operator == not defined on nil)

3.2 指针、管道、函数、map

管道、函数、map定义后编译器会返回指针类型,这些类型与nil进行比较等价与指针与nil进行比较,而指针与nil进行比较就是地址间的比较。这里需要注意,如果使用make()函数为map或管道分配了空间,则不为nil。

func main() {
    var a int = 0
    var p *int = &a
    fmt.Println(p == nil) // false
    p = (*int)(unsafe.Pointer(uintptr(0x0)))
    fmt.Println(p == nil) // true

    // ==================================

    m := make(map[int]int)
    fmt.Println(m == nil) // false
    c := make(chan int)
    fmt.Println(c == nil) // false
}

3.3 slice切片

上文中提到了slice底层的数据结构,可以看到数据结构中有三个属性,分别是指向底层数组的指针(数据存放的地址)、切片的长度和切片的容量,那么slice和nil比较究竟是比较什么呢?答案是,slice和nil进行比较实质上比较的是slcie结构体中指向数据的指针是否为nil,本质上也是指针的地址比较,可以从如下代码分析中得到结论。

func main() {
   var s []byte
   (*sliceTest)(unsafe.Pointer(&s)).len = 10
   fmt.Println(s == nil) // true
   (*sliceTest)(unsafe.Pointer(&s)).cap = 10
   fmt.Println(s == nil) // true
   (*sliceTest)(unsafe.Pointer(&s)).array = unsafe.Pointer(uintptr(0x1))
   fmt.Println(s == nil) // false
   (*sliceTest)(unsafe.Pointer(&s)).array = unsafe.Pointer(uintptr(0x0))
   fmt.Println(s == nil) // true
}

3.4 interface接口

先来看一下interface的底层数据结构,interface与nil进行比较比较的是结构体中指向类型的指针。当指向类型的指针为nil时,interface才为nil。在这个比较过程中指向数据的指针不参与比较。

// 空接口
type eface struct {
   _type *_type
   data  unsafe.Pointer
}
// 用int类型作为例子
type eface struct {
   _type *int
   data  unsafe.Pointer
}

func main() {
   var i interface{}
   fmt.Println(i == nil) // true
   (*eface)(unsafe.Pointer(&i)).data = unsafe.Pointer(uintptr(0x1))
   fmt.Println(i == nil) // true
   (*eface)(unsafe.Pointer(&i))._type = (*int)(unsafe.Pointer(uintptr(0x1)))
   fmt.Println(i == nil) // false
}

可以做一下如下思考题,检验一下是否完全理解了interface与nil的判断

func main() {
   var p *int
   fmt.Println(p == nil)                        // 1 true
   fmt.Println(p == (*int)(nil))                // 2 true
   fmt.Println((interface{})(p) == (*int)(nil)) // 3 true
   fmt.Println((interface{})(p) == nil)         // 4 false
}

答案是否与你所想一致呢?

大家可能对3号答案的结果不是很理解,我们可以先从4号结果进行分析,根据前面得到的结论,interface与nil进行比较时判断的是类型指针是否为nil,4号测试中,将结构体强转为interface类型,则会将结构体类型赋值给interface中的类型指针,这样interface就不为nil了。

3号测试中,等式左边强转后interface中的类型是*int,并且比较过程中使用类型来进行比较,等式右边是一个类型为*int的空指针,所以比较的最终结果为true。

四. 不同类型为nil时的特点

4.1 指针

  • 当指针为nil时不能对指针进行解引用
  • 结构体指针为nil时,能够调用结构体指针类型实现的方法,且该方法不能包含结构体的属性
  • 结构体指针为nil时,不能调用结构体类型实现的方法,也不能调用使用结构体属性的方法
type People struct {
   Name string
}

func (p *People) Born() {
   fmt.Println("Hello World~~~")
}

func (p *People) Born2() {
   fmt.Println("Hello World~~~", p.Name)
}

func (p People) Born3() {
   fmt.Println("Hello World~~~")
}

func main() {
   var pPeople *People

   // pass example
   pPeople.Born()

   // fail example
   // _ = *pPeople    // panic: runtime error: invalid memory address or nil pointer dereference
   // pPeople.Born2() // panic: runtime error: invalid memory address or nil pointer dereference
   // pPeople.Born3() // panic: runtime error: invalid memory address or nil pointer dereference
}

4.2 map

map为nil时能够进行读取,但不能进行写入。

func main() {
   var m map[int]int
   // 读
   _ = m[10]

   // 写
   m[10] = 10 // panic: assignment to entry in nil map
}

4.3 slice

slice为nil时不能进行直接的读写,但可以使用slice的内置函数append进行写入。

4.4 管道

  • 当一个chan为nil时,向chan中发送数据则会永远阻塞
  • 当一个chan为nil时,接收chan数据则会永远阻塞
  • close一个为nil的chan则会panic
  • 当一个chan为nil时,则会屏蔽select中的case
func main() {
   var c chan int
   // c <- 3   // fatal error: all goroutines are asleep - deadlock!
   // <-c      // fatal error: all goroutines are asleep - deadlock!
   // close(c) // panic: close of nil channel
}