关于你可能知道的 nil 的使用
关于 nil
nil
是啥?golang 中的 nil 相当于其他语言中的 NULL。
但 nil 的本质是啥?到底是一种类型?还是一种变量?还是一个关键字?
按照博主 奇伢云存储 的介绍,nil 是一个特殊变量,也是一种触发机制。
1 2 3 4 5 6 7 8
// 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 是一个 Type 型变量,Type 类型 是以 int 为基础的自定义类型。
但这不代表我们使用语言编程程序时能把 nil 当作一个变量。比如你把 nil 赋值给一个 byte 型变量,是不可以的,因为编译器不让。这是编译器的特权,你可没有!
这里明确一点,能够赋值 nil 的类型只有 “引用六兄弟 SMCIFP”:
slice
map
chan
interface
func
pointer
这就是编译器给你这位语言使用者的权限。在上面的源码中 var nil Type
后面的注释就有写。
所以除了这 6 兄弟之外,别的类型都不可以赋值 nil,或者和 nil 比较。
你可以把这 “引用六兄弟 SMCIFP” 理解为一种 nilable 的类型。
所谓 nilable,就是可以赋值为 nil(able to assign with nil),可以和 nil 比较(able to compare to nil)。
如果用 go 提案中用泛型表示,即:
1 2 3 4 5 6 7 8 9 10 11
type nilable interface { type []int, map[int]int, chan int, interface{}, unsafe.Pointer, func } func compare[T nilable] (a T) bool { return a == nil } func assign[T nilable] (a T) { a = nil }
并不严谨,仅仅用于表达 nilable 这个意思而已。
于是在编译器编译你的代码的时候,如果它看到你写出 a == nil
或 a = nil
这样的代码时,它要确认 a 是 nilable 的(是引用六兄弟 SMCIFP)。所以说 nil 是一种触发机制。
那么这六兄弟有什么特殊呢?
我们将 go 中的类型分为值类型
和引用类型
两类。
- 值类型就是 int 系列、uint 系列等;
- 引用类型就是 “引用六兄弟 SMCIFP”。
在 golang 中,使用变量前需要先声明变量。声明变量的本质是分配一块内存。
Clang(golang 的前辈)在声明变量是分配内存,但也只是分配内存,并不管这块内存里是不是干净的,所以会出现野指针此类问题。
我写 Clang 的时候就会有一个习惯,在声明变量的时候顺带初始化一个零值。
|
|
这样的习惯可以有效的预防一些意料之外的情况。
但是 golang 是二十一世纪的语言,它的变量声明在分配内存时是「置 0 分配」,也就是说 golang 可以保证你声明的变量分配的那块内存是干净的,你可以放心的使用。
所以在 golang 中声明一个变量但没有初始化,这个变量的值是其类型的零值,即:
|
|
SMCIFP
先声明一点:这六兄弟中,slice、map、chan、interface 是一种复合结构,pointer 和 func 是一个指针。
其实上面引用中的这句话已经能大概的看出答案了,因为他们是指针,所以是 nilable。我们可以看看编译器中这“六兄弟”的结构。
slice
|
|
map
|
|
chan
|
|
interface
|
|
func 和 pointer 本身就是一个指针。
可以看出,map 和 chan 两个都是比较大的结构体,而当你声明一个 map 或 chan 时,你必须通过 make()
才能使用。为什么?
因为这两个结构体比较大,为了提高性能,你声明的时候 var c chan int
的时候,c 是个指针,当你 make()
的时候才会分配内存,也就是初始化 hchan
。map 也是同样的道理。
像图中那样,slice、map、chan、interface 不管他们有多大,例如 slice 有 1000 个元素,map 有 10万对键值对,chan 缓存中有 100万 个数据,那都是他们背后的数组的大小;他们本身的大小,只有一个结构体那么大。
所以编译器在判断 nil 的时候其实非常快,因为这几个类型本身是指针,编译器只需要像如下判断即可:
- 对于 slice 的时候,判断 s 中的 array 字段是不是零值;
- 对于 map、chan 的时候,判断 m、c 自身是不是零值;
- 对于 interface 的时候,判断 i 自身是不是零值;
- 对于 pointer、func 的时候,判断 p、f 自身是不是零值;
同样,在赋值 nil 的时候,也是把他们自身赋值为 nil 即可。
除了 slice,因为它的结构比较小,总共就 24Byte(32位机中 12B),所以声明的时候直接初始化了一个 slice 结构,对 array 字段进行维护。
具体细节,推荐仔细阅读 https://mp.weixin.qq.com/s/sHLYy_4XA6254-vLmlu0IA,这篇文章讲的非常通俗和详细。
总结
- nil 只能用于引用六兄弟 SMCIFP;
- 变量只是一个指向一块内存的指针
- slice 结构较小,所以声明的时候就直接分配内存了,map 和 chan 需要 make() 才分配内存。
- 除了 pointer 和 func,其他都是使用对应结构体来管理
- 对 MCIFP 的判断 nil 和 赋值 nil,都是对变量本身进行操作,而 Slice 是对 array 字段进行操作。