一、nil 是什么东西?
1.1、nil 不是一个关键字
Golang 中的关键字一共有 25 个,并不包含 nil。
break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var
复制代码
而 nil 这个单词在 go 程序中是可以随意定义的,可以试一下下面这段代码:
func main() {
nil := "1"
fmt.Println(nil)
var slice []string = nil
fmt.Println(slice)
}
复制代码
1.2、是一个变量,是一个预先声明的标识符
// 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 只是一个 Type 类型的变量而已。在程序启动、变量初始化的时候,nil 变量被随机分配一个地址。而 Type 类型是基于 int 定义出来的一个新类型。
另外,源码注释里也指出了,nil 只能与”pointer, channel, func, interface, map, slice”六种类型进行比较,相关内容后文会进行阐述。用其他类型来和 nil 比较,那么在编译期间 typecheck 会报错。
1.3、为什么需要有 nil?
Go 分配内存是置 0 分配的(置 0 分配的意思是:确保分配出来的内存块里面是全 0 数据)。可以看到 runtime.makeslice、runtime.makechan 等函数中分配内存的时候,使用的 mallocgc 函数,其第三个参数是控制是否要清空分配的内存。相对的,C 语言默认分配内存的行为则仅仅是分配内存,里面的数据不能做任何假设,可能是全 0 ,可能是全 1。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
复制代码
而一些类型,如指针、slice,是没有一个明确的零值表示,此时需要借助 nil 进行零值判断。其实就是说 nil 是为 pointer/channel/func/interface/map/slice 类型预先声明的零值。
第二个作用就是将对象赋值为零值(nil),可以辅助 gc 进行内存回收,这点在 go map 分享中有讲过。再比如将 slice 置为 nil,就可以释放其底层引用的数组。
看见有人说将对象赋值为 nil 后,其占用内存就变为 0 了,但是通过 printf 打印出来的对象内存大小还是对象数据结构的大小。以及打印 nil 对象的地址,都变为 0x0。我个人感觉还是“nil 对象的地址指向 nil”、“nil 对象内存占用内存为 0”的说法更合理一点,打印结果不符合,大概是验证姿势不对?
1.4、思考题1
如下的声明/初始化代码能执行吗?
var val = nil
fmt.Printf("%T\n",val)
复制代码
——会报错“use of untyped nil”。只有 pointer/channel/func/interface/map/slice 六种类型可以使用 nil 进行比较、赋值。而这行代码信息过少,类型具有不确定性,编译器无法推断 nil 期望的类型。
二、与 nil 进行比较
对于 nil 实现逻辑的理解,我觉得“奇伢”大佬说的非常好,摘抄如下
在语言级别,nil 概念是由编译器带给你的。不是所有的类型都可以和
nil
进行比较或者赋值,只有这 6 种类型的变量才能和 nil 值比较,因为这是编译器决定的。同样的,不能赋值一个nil
变量给一个整型,原因也很简单,仅仅是编译器不让,就这么简单。
所以,nil
其实更准确的理解是一个触发条件,编译器看到和nil
值比较的写法,那么就要确认类型在这 6 种类型以内,如果是赋值nil
,那么也要确认在这 6 种类型以内。
2.1、nil 与 nil 的比较
fmt.Println(nil == nil)
复制代码
这可以理解为两个标识符在进行比较,会报错“invalid operation: nil == nil”。
2.2、nil 与指针的比较
指针是一个 8 字节的内存块,其值为指向对象的内存地址。
var ptr *int
复制代码
指针与 nil 的比较就是判断“ptr == 0”。
func main() {
var a = (*int64)(unsafe.Pointer(uintptr(0x0)))
fmt.Println(a == nil) //true
}
复制代码
函数类型的变量也是个指针,和 nil 的比较的逻辑是一样的。
2.3、nil 与 slice 的比较
slice 是一个结构体,无论是 var 初始化还是 make 初始化,这个结构体都已经分配好了,因此初始化变量都可以直接使用,比如 append 操作。有的人可能会认为 var 初始化的 slice 变量和 map 一样不能直接使用,这其实是不对的,从直接赋值操作的 panic 信息也可以看出:是“ index out of range”,而不是“invalid memory address or nil pointer dereference”———报错只是因为 var 初始化的 slice 变量没有合适的 length,var slice 就相当于是 make(slice, 0, 0)。
(对于 new slice 来说,得到的是一个指针,是不可以直接使用的。)
细究上文会发现一个问题:为什么 var s slice 得到的 s 变量是 nil 值,而 make(slice, 0, 0) 得到的不是 nil 值,两者不是一样的吗?
func main() {
a := make([]int, 0, 0)
if a == nil {
}
var b []int
if b == nil {
}
}
复制代码
其实这两个分别是 nil slice 和 empty slice,两者不同之处在于 make 函数会对 slice 的底层数据结构进行初始化,即初始化了 slice 底层依赖的数组。而编译器把“slice.array unsafe.Pointer == nil”作为了 slice == nil 的判断标准。即,slice 底层的动态数组指针没有实际数据的时候,slice 为 nil,不对 len 和 cap 两个字段做判断。实验代码如下:
type sliceType struct {
array unsafe.Pointer
len int
cap int
}
func main() {
var a []byte
((*sliceType)(unsafe.Pointer(&a))).len = 0x3
((*sliceType)(unsafe.Pointer(&a))).cap = 0x4
if a != nil {
println("not nil")
} else {
println("nil")
}
}
复制代码
2.4、nil 与 map/channel 的比较
map/channel 是一个指针,但是指向了一个结构体。 var 初始化得到的只是一个指针变量,该指针无法直接操作写(使用的话会报空指针的 panic),必须使用 make 初始化对其结构体进行内存分配后才可以使用。
所以 map/channel 和 nil 的比较,也是指针的比较,如下代码中的 m1 是 nil,而 m2 不是 nil。
var m1 map[string]int
var m2 = make(map[string]int)
复制代码
2.5、nil 与 interface 的比较
1、interface 什么时候是 nil
interface 由 type 和 data 两部分组成(无方法的空接口的结构体是 runtime.eface,有方法的接口的结构体是 runtime.iface,两者都是由这两部分组成)。
对于 interface 是否为 nil 值的判断,必须要两部分都为空,interface 才为 nil。其实有可能只需要判断 type 是否为 nil 即可,因为 type 未赋值时,其 data 肯定也是空的,但是这里还没有实验验证过,留个 todo 吧。
常见的一个错误 case 如下,将 interface 赋值为一个 nil 的 *struct,误认为 interface 也为 nil。
func main() {
err := GetErr()
fmt.Println(err == nil)
}
type Error interface {
}
type err struct {
Code int64
Msg string
}
func GetErr() Error {
var res *Error
return res
}
复制代码
当然,也可以借助反射去只判断 data 部分是否为 nil:
func IsNil(i interface{}) bool {
vi := reflect.ValueOf(i)
if vi.Kind() == reflect.Ptr {
return vi.IsNil()
}
return false
}
复制代码
2、关于 error interface 的一个建议
Do not return concrete error types. 下面是一个 bad case:
func main() {
fmt.Println( GetErr() == nil)
fmt.Println( WrapGetErr() == nil)
}
type Error interface {
}
type err struct {
Code int64
Msg string
}
func GetErr() *err {
return nil
}
func WrapGetErr() Error {
return GetErr()
}
复制代码
3、interface 什么时候等于 nil *struct
对下面这段代码,大家可以判断一下结果:
type A interface{}
type B struct{}
var a *B
fmt.Println(a == nil) // 1
fmt.Println(a == (*B)(nil)) // 2
fmt.Println((A)(a) == (*B)(nil)) // 3
fmt.Println((A)(a) == nil) // 4
复制代码
答案分别是 true,true,true,false,1/2/4都比较好理解吧,感觉主要是第三题的答案有点反直觉:明明 (A)(a) 不是 nil。原因是 interface 的语法:interface 与一个值做比较的时候,会同时比较 type 和 value,type 和 value都相等的时候才会认为两个对象相等。
4、为什么 nil *struct 不等于 nil interface?
Francesc Campoy:nil *struct 也是可以实现接口的,它作为接口,具有默认行为。但是 nil interface 就是无法操作的。
func doSum(s Summer) int {
if s == nil { // use nil interface to signal default
return 0
}
return s.Sum()
}
复制代码
2.6、nil 也是有类型的
如下,不同类型的 nil 值不能进行比较,当然 interface 和 struct 的比较除外。
func main() {
var ptr *int64 = nil
var cha chan int64 = nil
var fun func() = nil
var inter interface{} = nil
var m map[string]string = nil
var slice []int64 = nil
fmt.Println(ptr == cha)
fmt.Println(ptr == fun)
fmt.Println(ptr == inter)
fmt.Println(ptr == m)
fmt.Println(ptr == slice)
fmt.Println(cha == fun)
fmt.Println(cha == inter)
fmt.Println(cha == m)
fmt.Println(cha == slice)
fmt.Println(fun == inter)
fmt.Println(fun == m)
fmt.Println(fun == slice)
fmt.Println(inter == m)
fmt.Println(inter == slice)
fmt.Println(m == slice)
}
复制代码
运行上述代码,可以捕获第二个思考题:
为什么指针类型、channel类型可以和接口类型进行比较呢?
三、make the zero value useful
3.1、nil 在 go 中的含义到底是什么?
- Pointer 指向空对象
- Slice 底层数组为空
- Map 未初始化
- Channel 未初始化
- Function 未初始化
- Interface 未赋值
3.2、nil receiver is useful
*receiver(*表示指针对象) 为 nil 时,依旧可以调用其 func (*T) funtion 的方法,但是不能调用 func (T) funtion。这是因为 *receiver 执行 func (T) funtion 时,编译器做了一次语法糖:将 *receiver 拷贝了一份再进行调用,而当 *receiver==nil 时无法进行拷贝。
- 引申1:临时 receiver(表示非指针对象) 变量无法调用 func (*T) funtion 的方法,因为无法取地址。
- 引申2:*receiver 和 receiver 可以任意地使用 func (*T) funtion 和 func (T) funtion,因为编译器做了语法糖。
3.3、nil map 的使用
- map 未初始化的话,读/删不会报错,在 map 只读的场景减少了判断逻辑;
- map 未初始化的话,写会 panic。
3.4、用 nil 进行 channel 开关
思考下面这段代码有什么问题?
func merge(out chan<- int, a, b <-chan int) {
var aClosed, bClosed bool
for !aClosed || !bClosed {
select {
case v, ok := <- a:
if !ok { aClosed = true; continue }
out <- v
case v, ok := <- b:
if !ok { bClosed = true;continue }
out <- v
}
}
close(out)
}
复制代码
—— CPU 爆炸。
使用 nil 进行优化——use nil channel to disable a select case:
func merge(out chan<- int, a, b <-chan int) {
for a != nil || b != nil {
select {
case v, ok := <- a:
if !ok { a = nil; continue }
out <- v
case v, ok := <- b:
if !ok { b = nil; continue }
out <- v
}
}
close(out)
}
复制代码
另外,nil channel 和 closed channel 的几点使用说明:
- 写入 closed channel 的时候会 panic
- close 一个 nil/closed channel 会 panic
- 读写 nil channel 都会造成永远阻塞
3.5、nil function can be an indication of default values
func NewServer(logger function) {
if logger == nil {
logger = log.Printf // default
}
logger.DoSomething...
}
复制代码