一. 与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
}
复制代码
有疑问加站长微信联系(非本文作者)