Golang中nil用法详解
背景
大家好,今天给大家分享的知识点是 Go 中的nil
用法详解,在实际工作中,看到很多同学使用nil
的时候还是不太理解,导致了很多奇怪的问题发生(尤其是和nil
做比较经常出现走入到奇怪的分支)。
如果觉得一个技术点奇怪,不是我们所期望的效果,原因是我们对其原理不够了解,不够熟悉。 那今天就一起探索下这块的知识。
Go基础定义与类型
在 Go
语言中经常使用nil
,来表示多种类型的零值, 相信很多同学在使用过程中都踩过很多的坑,比如 nil
和其他类型比较,什么时候等,什么时候不等,经常会被用错,进而导致程序出现问题。
这篇文章希望梳理一下 nil
的用法和原理。
两个 nil 值未必相等案例
从工作中遇到的实际问题,如下的一段代码开始:
package main
import (
"fmt"
"reflect"
)
type MyError struct{}
func (me *MyError) Error() string {
return "my error"
}
func SomeThing() error {
var myError *MyError // 默认初始化为 nil
// ...
if myError == nil { // 条件成立
fmt.Println("myError is nil")
}
return myError
}
func main() {
err := SomeThing()
fmt.Println(reflect.TypeOf(err), reflect.ValueOf(err)) // *main.MyError <nil>
if err != nil { // 虽然没有返回,这里会被执行,因为 err 的类型不是 nil
fmt.Println(err)
}
}
运行结果是:
myError is nil
*main.MyError my error
my error
error
是一个接口类型, 它包含一个 Error()
方法,返回值为 string
。任何实现这个接口的类型都可以作为一个错误使用。 MyError
是实现了error
接口的类型。
出现这样结果的原因是:内外两个变量与nil
比较时类型不一样(里面是指针的比较,外部的比较是接口类型的比较)。在返回的时候出现了类型转换。 关于Go
类型四种类型转换,这里之前做过总结。Golang中四种类型转换详解
那紧接着疑惑就来了。 如果将返回error
接口换成 *MyError
,结果怎么样呢? 或者是我使用指针类型返回呢?等等改造。 那哪种方式是推荐的呢?
经过一顿的改造与尝试,似乎懂了,但似乎也没有吃透。 因此希望通过写出来方式梳理下自己思路。
nil 到底是怎么定义的?
下面是 buildin/buildin.go
中对于 nil
的定义。
// 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
从上面的定义看出,nil
是Go中内置的标识符,表示 pointer
, channel
, func
, interface
, map
, slice
类型的零值。 nil
并非 Go 中关键字。
小结:nil
是一个内置变量,编译器 遇到 与nil
值比较用户,会确认类型在这6种类型以内。同样赋值 nil
,那么也要确认在这6种类型以内,并且对应的结构内存为全0(或者是内存尚未被初始化)。
nil 的比较
在语言级别,
nil
概念是由编译器带给你的。不是所有的类型都可以和nil
进行比较或者赋值,只有这 6 种类型的变量才能和nil
值比较,因为这是编译器决定的。同样的,不能赋值一个nil
变量给一个整型,原因也很简单,仅仅是编译器不让,就这么简单。上面这段引用自xx技术大佬的原文。
nil
其实更准确的理解是一个触发条件,编译器看到和 nil
值比较的写法,那么就要确认类型在这 6 种类型以内,如果是赋值 nil
,那么也要确认在这 6 种类型以内。
nil == nil
: 这种方式也是不符合的,编辑器会报错 invalid operation: nil == nil
为了一探究竟,因此我们就不得不研究下 可以和nil
类型比较的6种类型的数据结构与定义。
nil 与指针比较
var a *context.Context
var b *int
// a == nil // true
// b == nil // true
// a == b // invalid operation: a == b (mismatched types *context.Context and *int)
这里的指针和C中指针很相似,也是一个 8
字节的内存块,其值为指向对象的内存地址。
同样的指针和nil
比较,就是判断指针指向的对象是否0
值,使用代码表示如下:
func main() {
var a = (*int64)(unsafe.Pointer(uintptr(0x0)))
fmt.Println(a == nil) //true
}
其他类型的变量也是个指针,和 nil
的比较的逻辑也是一样的。
nil 与 slice 比较
Go
中切片是对数组的抽象。与C
相比,Go
提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。 关于slice
内容,我们后边会有专门的篇幅进行详细介绍。
var slice1 []int
var slice2 = make([]int, 2)
var slice3 = new([]int)
fmt.Println(slice1 == nil) // ture
fmt.Printf("%v, %v\n",reflect.TypeOf(slice1), reflect.ValueOf(slice1)) // []int, []
fmt.Println(slice2 == nil) // false
fmt.Printf("%v, %v\n",reflect.TypeOf(slice2), reflect.ValueOf(slice2)) // []int, [0 0]
fmt.Println(slice3 == nil) // false
fmt.Printf("%v, %v\n",reflect.TypeOf(slice3), reflect.ValueOf(slice3)) //*[]int, &[]
fmt.Println(*slice3 == nil) // true
fmt.Printf("%v, %v\n",reflect.TypeOf(*slice3), reflect.ValueOf(*slice3)) //[]int, []
slice4 := append(*slice3, 5)
slice3 = nil
fmt.Printf("%v", slice4) //[5]
在解释上面的结果输出之前,我们先看下 src/runtime/slice.go
中的定义:
type slice struct {
array unsafe.Pointer
len int
cap int
}
array
:一个指针,指向底层存储数据的数组。
len
:切片的长度,在代码中我们可以使用len()
函数获取这个值。
cap
:切片的容量,即在不扩容的情况下,最多能容纳多少元素。在代码中我们可以使用cap()
函数获取这个值。
nil
与 slice类型的比较,其实是 array
字段是否被初始化(或者是指针是否被分配实际空间)。
输出分析:
slice1 == nil
,slice1
表示定义了一个类型为int
的切片, 此时并没有对array
字段进行内存空间分配,因此与nil
比较结果为true
。slice2 == nil
,slice2
是使用make
进行定义,make
的语法为:func make(t Type, size …IntegerType) Type
。 返回类型已经对array
字段进行分配内存。 因此返回为false
。slice3 == nil
,slice3
使用new([]int)
定义,new
的语法为:func new(Type) *Type;
。 返回值是一个指针,指针的类型指向[]int
类型。 因此这个比较其实是指针类型的与nil的比较 根据函数定义,可以看出返回的指针已经被初始化。 因此返回为false
。*slice3 == nil
,经过取值后,变成了slice
类型,与nil
的比较。此时的array尚未被初始化,因此结果为true
slice3 = nil
, 这个赋值操作可以将对array
变量的指针引用量减1,方便GC
进行垃圾回收。
小结:
- 如果当前是
slice
类型与nil
比较,判断的其实是array
字段是否为空。 - 如果当前是指向
slice
类型指针与nil
比较,遵循的其实是ptr
类型与nil
的比较。
【下面是slice引申,与主题无关可以跳过】 其实这里有个很神奇的变量slice3
, 使用new
定义他返回的是24字节长度的 slice
类型并不是指针。 这解释了解释为什么slice
new后可以直接使用,map就不行。
var aslice = new([]int)
fmt.Println(append(*aslice, 1))
var amap = new(map[string]string)
(*amap)["a"] = "a" // panic
nil 与 map 的比较
Go map
底层实现方式是 Hash
表。
// A header for a Go map.
type hmap struct {
count int // 元素的个数。len() 函数返回的就是这个值
flags uint8 // 状态标记位。如是否被多线程读写、迭代器在使用新桶、迭代器在使用旧桶等
B uint8 // 桶指数,表示 hash 数组中桶数量为 2^B(不包括溢出桶)。最大可存储元素数量为 loadFactor * 2^B
noverflow uint16 // 溢出桶的数量的近似值。详见函数 incrnoverflow()
hash0 uint32 // hash种子
buckets unsafe.Pointer // 指向2^B个桶组成的数组的指针。可能是 nil 如果 count 为 0
oldbuckets unsafe.Pointer // 指向长度为新桶数组一半的旧桶数组,仅在增长时为非零
nevacuate uintptr // 进度计数器,表示扩容后搬迁的进度(小于该数值的桶已迁移)
extra *mapextra // 可选字段, extra.overflow:保存溢出桶链表, extra.oldoverflow:保存旧溢出桶链表, extra.nextOverflow:下一个空闲溢出桶地址
}
下面是map数据结构图:
下面是map的定义与nil
值的比较 【如果觉得麻烦的可以直接跳过去看结论,里面写了很多对结构体指针进行探索的内容】
// 第一种定义方法
var map1 map[string]string
fmt.Println(map1 == nil) // true
fmt.Printf("%v, %v, %d\n",reflect.TypeOf(map1), reflect.ValueOf(map1), len(map1)) // map[string]string, map[], 0
// map1["0"] = "1" // assignment to entry in nil map
// 第二种定义方法
map2 := map[string]int{
"1": 2,
"3": 4,
"5": 6,
}
fmt.Println(map2 == nil)// false
fmt.Println(map2, len(map2)) // map[1:2 3:4 5:6] 3
fmt.Printf("%v, %v\n",reflect.TypeOf(map2), reflect.ValueOf(map2)) // map[string]int, map[1:2 3:4 5:6]
// 第二种定义方法,只是初始化值为空
map3 := map[string]int{}
fmt.Println(map3 == nil)// false
fmt.Println(map3, len(map3)) // map[] 0
fmt.Printf("%v, %v\n",reflect.TypeOf(map3), reflect.ValueOf(map3)) // map[string]int, map[]
// 第三种方法
map4 := new(map[string]int)
fmt.Println(map4 == nil) //false
fmt.Printf("%v, %v\n",reflect.TypeOf(map4), reflect.ValueOf(map4)) // *map[string]int, &map[]
// (*map4)["a"] = 1 // panic: assignment to entry in nil map
//!!! 下面的方法是对 map类型的指针的探索与研究,感兴趣的同学可以看下,不感兴趣跳过也也不影响阅读。
var ptr = unsafe.Pointer(&map2)
fmt.Printf("%v, %v\n", reflect.TypeOf(ptr), reflect.ValueOf(ptr)) // unsafe.Pointer, 0xc000056028
var ptrMap = new(map[string]int)
fmt.Printf("%v, %v\n", reflect.TypeOf(ptrMap), reflect.ValueOf(ptrMap)) // *map[string]int, &map[]
ptrMap = (*map[string]int)(ptr)
fmt.Printf("map length is: %d\n", len(*ptrMap)) // map length is: 3
// 尝试对ptr进行操作
fmt.Printf("宽度为: %d\n", unsafe.Sizeof(ptr))
var ptr2 = unsafe.Add(ptr, 8) // 尝试对指针进行偏移计算,因为第一个是count
var ptrint *int = (*int)(ptr2)
fmt.Printf("%d\n", *ptrint)
// invalid operation: cannot indirect ptr (variable of type unsafe.Pointer) Go不支持对指针进行此类算术运算。任何此类操作都将导致编译时错误
// fmt.Printf("xxx: %d", int(*(ptr+8)))
fmt.Printf("长度为2: %d\n", unsafe.Sizeof(map2))
var ptr3 = (*int)(unsafe.Pointer(&map2))
fmt.Printf("*ptr3: %d", *ptr3)
结论:
-
使用
make
方式定义,根据func makemap(t *maptype, hint int, h *hmap) *hmap
函数make
返回值可以看出,map
是一个指针,但是指向了一个结构体(hmap
)。 -
var
初始化得到的只是一个指针变量,该指针无法直接操作写(使用的话会报空指针的panic
),必须使用make
初始化对其结构体进行内存分配后才可以使用。 -
new
关键字定义,返回的值是map
类型指针,因此与nil的比较为true
。
因此使用make
定义的map
, 即使map的长度为0,与nil
比较,值也是不相同的。 nil
表示是否对map
结构是否初始化的标识。
【思考】slice
和 map
分别作为函数参数时有什么区别?
分别看下slice
和 map
中 使用make 方法定义的区别:
func makeslice(et *_type, len, cap int) slice
, 函数返回的是Slice
结构体。func makemap(t *maptype, hint int, h *hmap) *hmap
, 函数返回的是*hmap
指针
Go中的参数传递实际都是值传递,将slice
作为参数传递时,函数中会创建一个slice
参数的副本,这个副本同样也包含array,len,cap
这三个成员。
副本中的array
指针与原slice
指向同一个地址,所以当修改副本slice
的元素时,原slice
的元素值也会被修改。但是如果修改的是副本slice
的len
和cap
时,原slice
的len
和cap
仍保持不变。
如果在操作副本时由于扩容操作导致重新分配了副本slice
的array
内存地址,那么之后对副本slice
的操作则完全无法影响到原slice
,包括slice
中的元素。
关于slice
的使用,如果使用安全与性能下期会讲到。
nil 与 channel 的比较
channel
使用make
定义 func makechan(t *chantype, size int) *hchan
var chan1 chan int
fmt.Println(chan1 == nil) // true
chan2 := make(chan int, 0)
fmt.Println(chan2 == nil) // false
chan3 := new(chan int)
fmt.Println(chan3 == nil) // false
nil
与 channel
的比较,结果表现与map
类型相似。
nil 与 interface{} 的比较
interface
是一组 method
签名的组合,我们通过 interface
来定义对象的一组行为,只要实现了接口的所有方法,就表示该类型实现了该接口。
例如:下面关于 Animal
的接口。
type Animal interface {
Eat(string) string
Drink(string) string
}
interface{}
: 表示空接口,空接口可用于保存任何数据。
interface
有两种类型来实现的: runtime.iface
和 runtime.eface
。区别就是接口中是否包含了方法。 iface
和 eface
都是由两个子类型组合而成。
eface
:_type
+data
, 其中_type
数据结构主要是对data结构的描述
iface
:itab
+data
,其中itab
里面包含了_type
结构。 并且多了对方法的描述。
对于 interface
是否为 nil
值的判断,必须要两部分都为空,interface
才为 nil
。 其实只要tpye
为 nil
, 那么结果就一定为nil
。
案例分析
学废没学废,用下面的案例验证一下就知道了。
案例1:
func testInterface() interface{} {
var c *Cat
//fmt.Printf("%v, %v \n", reflect.TypeOf(c), reflect.ValueOf(c))
fmt.Printf("c is nil: %v\n", c == nil) // true
return c
}
func main(){
test := testInterface()
fmt.Printf("%v, %v \n", reflect.TypeOf(test), reflect.ValueOf(test)) // *main.Cat, *main.Cat
if test == nil {
fmt.Println("test is nil")
} else {
fmt.Println("test is not nil")
}
}
执行结果为test is not nil
。解析:
testInterface
函数中c == nil
进行比较,实际上 指针类型与nil
进行比较。main
中的比较实际上interface{}
类型与nil
比较,此时的interface{}
类型是*main.Cat
,因此结果是false
案例2:
type State struct{}
func testnil1(a, b interface{}) bool {
return a == b
}
func testnil2(a *State, b interface{}) bool {
return a == b
}
func testnil3(a interface{}) bool {
return a == nil
}
func testnil4(a *State) bool {
return a == nil
}
func testnil5(a interface{}) bool {
v := reflect.ValueOf(a)
return !v.IsValid() || v.IsNil() // reflect.IsValid()函数用于检查v是否表示一个值。
}
func main() {
var a *State
fmt.Println(testnil1(a, nil))
fmt.Println(testnil2(a, nil))
fmt.Println(testnil3(a))
fmt.Println(testnil4(a))
fmt.Println(testnil5(a))
}
结果为:
false,false,false,true,true
案例3:
func Foo() error {
var err *os.PathError = nil
// …
return err
}
func main() {
err := Foo()
fmt.Println(err)
fmt.Printf("%v, %v\n", reflect.TypeOf(err), reflect.ValueOf(err))
fmt.Println(err == nil)
fmt.Println(err == (*os.PathError)(nil))
}
输出结果为:
<nil>
*fs.PathError
<nil>
false
true
下面一片英文文章中的解释,原文解释的挺好,就不译了。
An interface value is equal to nil only if both its value and dynamic type are nil.
In the example above, Foo() returns[*os.PathError, nil]
and we compare it with [nil, nil]
.
You can think of the interface value nil as typed, and nil without type doesn’t equal nil with type. If we convert nil to the correct type, the values are indeed equal.
其实:这就是为什么有些同学说,如果 需要返回nil
的时候一定要直接返回 nil
。不要使用带类型的nil
。
案例4:
package main
import "fmt"
type IPeople interface {
hello()
}
type People struct {
}
func (p *People) hello() {
fmt.Println("hello")
}
func errFunc1(in int) *People {
if in == 0 {
fmt.Println("importantFunc返回了一个nil")
return nil
} else {
fmt.Println("importantFunc返回了一个非nil值")
return &People{}
}
}
func main() {
var i IPeople
in := 0
i = errFunc1(in)
if i == nil {
fmt.Println("哈,外部接收到也是nil")
} else {
fmt.Println("咦,外部接收到不是nil哦")
fmt.Printf("%v, %T\n", i, i)
}
}
这段代码的执行结果为:
importantFunc返回了一个nil
咦,外部接收到不是nil哦
<nil>, *main.People
可以看到在main
函数中收到的返回值不是nil
, 在errFunc1()
函数中返回的是nil
,到了main
函数为什么收到的不是nil
呢?
这是因为:将nil
赋值给*People
后再将*People
赋值给interface
,*People
本身是是个指向nil
的指针,但是将其赋给接口时只是接口中的值为nil
,但是接口中的类型信息为*main.People
而不是nil
,所以这个接口不是nil
。
Golang中的interface
类型包含两部分信息——值信息和类型信息,只有interface
的值合并类型都为nil
时interface
才为nil
,interface
底层实现可以在后面的源码分析看到。
所以正确的方式是:
func rightFunc(in int) IPeople {
if in == 0 {
fmt.Println("importantFunc返回了一个nil")
return nil
} else {
fmt.Println("importantFunc返回了一个非nil值")
return &People{}
}
}
直接将nil
赋给interface
类型就可以了。
怎么解决 interface 和 nil 的比较?
先将 interface
值转化为 reflect.Value
,然后借用IsNil
来判断是否为空即可。
但事实上,使用reflect
包下的方法一定要小心,此处入参 i
的类型为 interface{}
,也就意味着任何类型的值传进来皆可,贸然使用反射,容易引发 panic
。
The argument must be a chan, func, interface, map, pointer, or slice value; if it is not, IsNil panics
因此修改后的代码如下:
func isNilFixed(i interface{}) bool {
if i == nil {
return true
}
switch reflect.TypeOf(i).Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return reflect.ValueOf(i).IsNil()
}
return false
}
nil 在 go 中的含义到底是什么?
nil
仅仅可以与下面的6种类型,进行 赋值、比较。
Pointer
指向空对象。Pointer
的8
字节Slice
底层数组为空。slice
的24
字节管理结构Map
未初始化。map
的8
字节指针Channel
未初始化。channel
的8
字节指针Function
未初始化。function
的8
字节指针Interface
未赋值。interface
的16
字节
nil
对以上 6 种变量赋值 nil
的行为都是把变量本身置 0
,仅此而已。
nil
进行比较判断本质上都是和变量本身做判断,slice
是判断管理结构的第一个指针字段,map
,channel
本身就是指针,interface
也是判断管理结构的第一个指针字段,指针和函数变量本身就是指针;
提起nil
就再次拿出 nil channel
和 closed channel
的几点规则:
- 写入
closed channel
的时候会panic
。 close
一个nil/closed channel
会panic
- 读写
nil channel
都会造成永远阻塞
为什么需要 nil?
Go
分配内存是置 0
分配的。C 语言默认分配内存的行为则仅仅是分配内存,里面的数据不能做任何假设,可能是全 0
,可能是全 1
。
回顾在C语言中我们会使用下方法,动态申请内存:
int *p = (int *)malloc(sizeof(int));
*p = 100;
free(p);
p = NULL; // free函数在释放空间之后,把内存前的标志变为0,确保下次分配内存都是0值
return 0;
但Go中为了降低开发难度,减轻内存释放心智,采用了自动垃圾回收的方式,同时还引入了defer
用于资源的释放。 Go语言现在用的三色标记法(属于追踪式垃圾回收算法的一种, 后边会有文章详细介绍)。
因此在Go中,需要将对象赋值为零值(nil
),可以辅助 GC
进行内存回收。再比如将 slice
置为 nil
,就可以释放其底层引用的数组。
一些特殊的类型,如指针
、slice
,是没有一个明确的零值表示,此时需要借助 nil
进行零值判断。其实就是说 nil
是为 pointer/channel/func/interface/map/slice
类型预先声明的零值。
因此只有 pointer/channel/func/interface/map/slice
(我快速识记的方式是 mscfip
) 六种类型可以使用 nil
进行比较、赋值。而这行代码信息过少,类型具有不确定性,编译器无法推断 nil
期望的类型。
如果使用nil
赋值,编译器会报错
var val = nil //“use of untyped nil”。
相关阅读
- 图解Go语言interface底层实现
- 关于 interface{} 会有啥注意事项?上 感觉写的有点问题, 并不是因为没有方法导致的与
nil
结果不一致。 - 深入了解Go的interface{}底层原理 讲了底层原理,也讲了尽量不要使用接口转化,会有性能问题。
- Golang interface接口深入理解 里面有大量的转化题目
- 深入理解Go的interface{}内部执行原理 讲了Go的
interface
是由两种类型来实现的:iface
和eface
。 - Golang中的空接口 interface{}讲了空接口和普通类型之间的转换。包含了编程的细节,推荐大家阅读一下。
- Go nil 是什么?
- 深度解密Go语言之 map 原理性文章,写的很深,了解原理建议深读。
- Go “一个包含nil指针的接口不是nil接口”踩坑
- Golang 中的 nil 用法解析
- 深度剖析 Go 的 nil 这篇文章奇伢作者原创,写了很多自己理解,值的推荐。
- Go 接口:nil接口为什么不等于nil?
- 你会处理 go 中的 nil 吗 只是写出了遇到的问题,看上去模棱两可的解决问题,但是病没有说明原理。