前言
nil是什么?有的认为是0,有的认为是JAVA里面的NULL,我们从nil的定义出发,探索不同类型nil值的内存结构、nil判断逻辑。
nil定义
nil在golang中是一个预声明的标识符,表示指针、channel、函数、接口、map、slice的零值。它和true和false有些类似,但它没有明确的类型,需要在编译时,由编译器推导,然后转成明确类型的零值。
golang
// 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
nil内存大小
从定义上看,nil只是个没有类型的标识符,那么,当一个对象被nil赋值时,编译器是如何生成机器代码的呢? 当编译器转换variable = nil代码时,会推断variable的类型,生成对应类型的零值,所以nil赋值给不同类型变量时,其大小有可能不一样。具体而言: 那么,其具体的内存结构又是怎样的呢?我们从汇编代码的角度来看看不同的类型,其内存结构如何:
scss
var p *struct{} = nil
0x1000a36d0 f90013ff MOVD ZR, 32(RSP)
fmt.Println(unsafe.Sizeof(p)) // 8
0x1000a36d4 a9087fff STP (ZR, ZR), 128(RSP)
0x1000a36d8 910203e0 ADD $128, RSP, R0
0x1000a36dc f90023e0 MOVD R0, 64(RSP)
0x1000a36e0 3980001b MOVB (R0), R27
0x1000a36e4 900000c3 ADRP 98304(PC), R3
0x1000a36e8 91340063 ADD $3328, R3, R3
0x1000a36ec f90043e3 MOVD R3, 128(RSP)
0x1000a36f0 b0000063 ADRP 53248(PC), R3
0x1000a36f4 91290063 ADD $2624, R3, R3
0x1000a36f8 f90047e3 MOVD R3, 136(RSP)
0x1000a36fc 3980001b MOVB (R0), R27
0x1000a3700 14000001 JMP 1(PC)
0x1000a3704 f90063e0 MOVD R0, 192(RSP)
0x1000a3708 b24003e2 ORR $1, ZR, R2
0x1000a370c f90067e2 MOVD R2, 200(RSP)
0x1000a3710 f9006be2 MOVD R2, 208(RSP)
0x1000a3714 aa0203e1 MOVD R2, R1
0x1000a3718 97ffedda CALL fmt.Println(SB)
var s []int = nil
0x1000a371c f9004bff MOVD ZR, 144(RSP)
0x1000a3720 a909ffff STP (ZR, ZR), 152(RSP)
fmt.Println(unsafe.Sizeof(s)) // 24
0x1000a3724 a9087fff STP (ZR, ZR), 128(RSP)
0x1000a3728 910203e0 ADD $128, RSP, R0
0x1000a372c f90037e0 MOVD R0, 104(RSP)
0x1000a3730 3980001b MOVB (R0), R27
0x1000a3734 900000c3 ADRP 98304(PC), R3
0x1000a3738 91340063 ADD $3328, R3, R3
0x1000a373c f90043e3 MOVD R3, 128(RSP)
0x1000a3740 b0000063 ADRP 53248(PC), R3
0x1000a3744 91292063 ADD $2632, R3, R3
0x1000a3748 f90047e3 MOVD R3, 136(RSP)
0x1000a374c 3980001b MOVB (R0), R27
0x1000a3750 14000001 JMP 1(PC)
0x1000a3754 f90057e0 MOVD R0, 168(RSP)
0x1000a3758 b24003e2 ORR $1, ZR, R2
0x1000a375c f9005be2 MOVD R2, 176(RSP)
0x1000a3760 f9005fe2 MOVD R2, 184(RSP)
0x1000a3764 aa0203e1 MOVD R2, R1
0x1000a3768 97ffedc6 CALL fmt.Println(SB)
var m map[int]bool = nil
0x1000a376c f90017ff MOVD ZR, 40(RSP)
fmt.Println(unsafe.Sizeof(m)) // 8
0x1000a3770 a9087fff STP (ZR, ZR), 128(RSP)
0x1000a3774 910203e0 ADD $128, RSP, R0
0x1000a3778 f90033e0 MOVD R0, 96(RSP)
0x1000a377c 3980001b MOVB (R0), R27
0x1000a3780 900000c3 ADRP 98304(PC), R3
0x1000a3784 91340063 ADD $3328, R3, R3
0x1000a3788 f90043e3 MOVD R3, 128(RSP)
0x1000a378c b0000063 ADRP 53248(PC), R3
0x1000a3790 91290063 ADD $2624, R3, R3
0x1000a3794 f90047e3 MOVD R3, 136(RSP)
0x1000a3798 3980001b MOVB (R0), R27
0x1000a379c 14000001 JMP 1(PC)
0x1000a37a0 f90093e0 MOVD R0, 288(RSP)
0x1000a37a4 b24003e2 ORR $1, ZR, R2
0x1000a37a8 f90097e2 MOVD R2, 296(RSP)
0x1000a37ac f9009be2 MOVD R2, 304(RSP)
0x1000a37b0 aa0203e1 MOVD R2, R1
0x1000a37b4 97ffedb3 CALL fmt.Println(SB)
var c chan string = nil
0x1000a37b8 f9001fff MOVD ZR, 56(RSP)
fmt.Println(unsafe.Sizeof(c)) // 8
0x1000a37bc a9087fff STP (ZR, ZR), 128(RSP)
0x1000a37c0 910203e0 ADD $128, RSP, R0
0x1000a37c4 f9002fe0 MOVD R0, 88(RSP)
0x1000a37c8 3980001b MOVB (R0), R27
0x1000a37cc 900000c3 ADRP 98304(PC), R3
0x1000a37d0 91340063 ADD $3328, R3, R3
0x1000a37d4 f90043e3 MOVD R3, 128(RSP)
0x1000a37d8 b0000063 ADRP 53248(PC), R3
0x1000a37dc 91290063 ADD $2624, R3, R3
0x1000a37e0 f90047e3 MOVD R3, 136(RSP)
0x1000a37e4 3980001b MOVB (R0), R27
0x1000a37e8 14000001 JMP 1(PC)
0x1000a37ec f90087e0 MOVD R0, 264(RSP)
0x1000a37f0 b24003e2 ORR $1, ZR, R2
0x1000a37f4 f9008be2 MOVD R2, 272(RSP)
0x1000a37f8 f9008fe2 MOVD R2, 280(RSP)
0x1000a37fc aa0203e1 MOVD R2, R1
0x1000a3800 97ffeda0 CALL fmt.Println(SB)
var f func() = nil
0x1000a3804 f9001bff MOVD ZR, 48(RSP)
fmt.Println(unsafe.Sizeof(f)) // 8
0x1000a3808 a9087fff STP (ZR, ZR), 128(RSP)
0x1000a380c 910203e0 ADD $128, RSP, R0
0x1000a3810 f9002be0 MOVD R0, 80(RSP)
0x1000a3814 3980001b MOVB (R0), R27
0x1000a3818 900000c3 ADRP 98304(PC), R3
0x1000a381c 91340063 ADD $3328, R3, R3
0x1000a3820 f90043e3 MOVD R3, 128(RSP)
0x1000a3824 b0000063 ADRP 53248(PC), R3
0x1000a3828 91290063 ADD $2624, R3, R3
0x1000a382c f90047e3 MOVD R3, 136(RSP)
0x1000a3830 3980001b MOVB (R0), R27
0x1000a3834 14000001 JMP 1(PC)
0x1000a3838 f9007be0 MOVD R0, 240(RSP)
0x1000a383c b24003e2 ORR $1, ZR, R2
0x1000a3840 f9007fe2 MOVD R2, 248(RSP)
0x1000a3844 f90083e2 MOVD R2, 256(RSP)
0x1000a3848 aa0203e1 MOVD R2, R1
0x1000a384c 97ffed8d CALL fmt.Println(SB)
var i interface{} = nil
0x1000a3850 a9077fff STP (ZR, ZR), 112(RSP)
fmt.Println(unsafe.Sizeof(i)) // 16
0x1000a3854 a9087fff STP (ZR, ZR), 128(RSP)
0x1000a3858 910203e0 ADD $128, RSP, R0
0x1000a385c f90027e0 MOVD R0, 72(RSP)
0x1000a3860 3980001b MOVB (R0), R27
0x1000a3864 900000c3 ADRP 98304(PC), R3
0x1000a3868 91340063 ADD $3328, R3, R3
0x1000a386c f90043e3 MOVD R3, 128(RSP)
0x1000a3870 b0000063 ADRP 53248(PC), R3
0x1000a3874 91294063 ADD $2640, R3, R3
0x1000a3878 f90047e3 MOVD R3, 136(RSP)
0x1000a387c 3980001b MOVB (R0), R27
0x1000a3880 14000001 JMP 1(PC)
0x1000a3884 f9006fe0 MOVD R0, 216(RSP)
0x1000a3888 b24003e2 ORR $1, ZR, R2
0x1000a388c f90073e2 MOVD R2, 224(RSP)
0x1000a3890 f90077e2 MOVD R2, 232(RSP)
0x1000a3894 aa0203e1 MOVD R2, R1
0x1000a3898 97ffed7a CALL fmt.Println(SB)
代码说明:
- 指针类型
*struct{}
:
csharp
var p *struct{} = nil
fmt.Println(unsafe.Sizeof(p)) // 8
在 64 位系统中,指针类型通常占用8个字节,因为它们存储的是内存地址,其nil值为0。
- 切片类型
[]int
csharp
var s []int = nil
fmt.Println(unsafe.Sizeof(s)) // 24
切片在 Go 中是一个结构体,包含指向底层数组的指针、切片的长度和容量。因此,即使切片为 nil
,它的大小也是 24 字节(在 64 位系统中,每个指针和整数占用 8 个字节),其nil值每个字段均为0。
- 映射类型
map[int]bool
go
var m map[int]bool = nil
fmt.Println(unsafe.Sizeof(m)) // 8
映射在 Go 中是一个引用类型,本质上是一个指向哈希表的指针。因此,它的大小为 8 个字节(在 64 位系统中),其nil值为空指针。
- 通道类型
chan string
go
var c chan string = nil
fmt.Println(unsafe.Sizeof(c)) // 8
通道也是引用类型,其值是一个指向底层数据结构的指针,所以大小为 8 个字节(在 64 位系统中),其nil值为空指针。
- 函数类型
func()
csharp
var f func() = nil
fmt.Println(unsafe.Sizeof(f)) // 8
函数类型在 Go 中也是一个指针,指向函数的入口地址,因此大小为 8 个字节(在 64 位系统中),其nil值为空指针。
- 接口类型
interface{}
:
csharp
var i interface{} = nil
fmt.Println(unsafe.Sizeof(i)) // 16
接口在 Go 中是一个包含两个字段的结构体:一个是指向类型信息的指针,另一个是指向具体值的指针。因此,即使接口为 nil
,它的大小也是 16 字节(在 64 位系统中,每个指针占用 8 个字节),类型指针和值指针均为空指针代表零值。
nil判断
了解不同类型nil值内存结构后,如何判断一个变量是否为空值呢?即:variable == nil是如何判断的呢? 对应的汇编代码:
scss
var p *struct{} = getStructPtr()
0x1000a3800 97ffffac CALL main.getStructPtr(SB)
0x1000a3804 f90013e0 MOVD R0, 32(RSP)
if p == nil {
0x1000a3808 b5000040 CBNZ R0, 2(PC)
0x1000a380c 14000002 JMP 2(PC)
0x1000a3810 14000014 JMP 20(PC)
fmt.Println("p is nil")
0x1000a3814 a9087fff STP (ZR, ZR), 128(RSP)
0x1000a3818 910203e0 ADD $128, RSP, R0
0x1000a381c f90023e0 MOVD R0, 64(RSP)
0x1000a3820 3980001b MOVB (R0), R27
0x1000a3824 900000c3 ADRP 98304(PC), R3
0x1000a3828 912d0063 ADD $2880, R3, R3
0x1000a382c f90043e3 MOVD R3, 128(RSP)
0x1000a3830 b0000143 ADRP 167936(PC), R3
0x1000a3834 912e8063 ADD $2976, R3, R3
0x1000a3838 f90047e3 MOVD R3, 136(RSP)
0x1000a383c 3980001b MOVB (R0), R27
0x1000a3840 14000001 JMP 1(PC)
0x1000a3844 f90063e0 MOVD R0, 192(RSP)
0x1000a3848 b24003e2 ORR $1, ZR, R2
0x1000a384c f90067e2 MOVD R2, 200(RSP)
0x1000a3850 f9006be2 MOVD R2, 208(RSP)
0x1000a3854 aa0203e1 MOVD R2, R1
0x1000a3858 97ffed8a CALL fmt.Println(SB)
0x1000a385c 14000001 JMP 1(PC)
var s []int = getSlice()
0x1000a3860 97ffffa0 CALL main.getSlice(SB)
0x1000a3864 f9004be0 MOVD R0, 144(RSP)
0x1000a3868 f9004fe1 MOVD R1, 152(RSP)
0x1000a386c f90053e2 MOVD R2, 160(RSP)
if s == nil {
0x1000a3870 b5000040 CBNZ R0, 2(PC)
0x1000a3874 14000002 JMP 2(PC)
0x1000a3878 14000014 JMP 20(PC)
fmt.Println("s is nil")
0x1000a387c a9087fff STP (ZR, ZR), 128(RSP)
0x1000a3880 910203e0 ADD $128, RSP, R0
0x1000a3884 f90037e0 MOVD R0, 104(RSP)
0x1000a3888 3980001b MOVB (R0), R27
0x1000a388c 900000c3 ADRP 98304(PC), R3
0x1000a3890 912d0063 ADD $2880, R3, R3
0x1000a3894 f90043e3 MOVD R3, 128(RSP)
0x1000a3898 b0000143 ADRP 167936(PC), R3
0x1000a389c 912ec063 ADD $2992, R3, R3
0x1000a38a0 f90047e3 MOVD R3, 136(RSP)
0x1000a38a4 3980001b MOVB (R0), R27
0x1000a38a8 14000001 JMP 1(PC)
0x1000a38ac f90057e0 MOVD R0, 168(RSP)
0x1000a38b0 b24003e2 ORR $1, ZR, R2
0x1000a38b4 f9005be2 MOVD R2, 176(RSP)
0x1000a38b8 f9005fe2 MOVD R2, 184(RSP)
0x1000a38bc aa0203e1 MOVD R2, R1
0x1000a38c0 97ffed70 CALL fmt.Println(SB)
0x1000a38c4 14000001 JMP 1(PC)
var m map[int]bool = getMap()
0x1000a38c8 97ffff96 CALL main.getMap(SB)
0x1000a38cc f90017e0 MOVD R0, 40(RSP)
if m == nil {
0x1000a38d0 b5000040 CBNZ R0, 2(PC)
0x1000a38d4 14000002 JMP 2(PC)
0x1000a38d8 14000014 JMP 20(PC)
fmt.Println("m is nil")
0x1000a38dc a9087fff STP (ZR, ZR), 128(RSP)
0x1000a38e0 910203e0 ADD $128, RSP, R0
0x1000a38e4 f90033e0 MOVD R0, 96(RSP)
0x1000a38e8 3980001b MOVB (R0), R27
0x1000a38ec 900000c3 ADRP 98304(PC), R3
0x1000a38f0 912d0063 ADD $2880, R3, R3
0x1000a38f4 f90043e3 MOVD R3, 128(RSP)
0x1000a38f8 b0000143 ADRP 167936(PC), R3
0x1000a38fc 912f0063 ADD $3008, R3, R3
0x1000a3900 f90047e3 MOVD R3, 136(RSP)
0x1000a3904 3980001b MOVB (R0), R27
0x1000a3908 14000001 JMP 1(PC)
0x1000a390c f90093e0 MOVD R0, 288(RSP)
0x1000a3910 b24003e2 ORR $1, ZR, R2
0x1000a3914 f90097e2 MOVD R2, 296(RSP)
0x1000a3918 f9009be2 MOVD R2, 304(RSP)
0x1000a391c aa0203e1 MOVD R2, R1
0x1000a3920 97ffed58 CALL fmt.Println(SB)
0x1000a3924 14000001 JMP 1(PC)
var c chan string = getChannel()
0x1000a3928 97ffff8a CALL main.getChannel(SB)
0x1000a392c f9001fe0 MOVD R0, 56(RSP)
if c == nil {
0x1000a3930 b5000040 CBNZ R0, 2(PC)
0x1000a3934 14000002 JMP 2(PC)
0x1000a3938 14000014 JMP 20(PC)
fmt.Println("c is nil")
0x1000a393c a9087fff STP (ZR, ZR), 128(RSP)
0x1000a3940 910203e0 ADD $128, RSP, R0
0x1000a3944 f9002fe0 MOVD R0, 88(RSP)
0x1000a3948 3980001b MOVB (R0), R27
0x1000a394c 900000c3 ADRP 98304(PC), R3
0x1000a3950 912d0063 ADD $2880, R3, R3
0x1000a3954 f90043e3 MOVD R3, 128(RSP)
0x1000a3958 b0000143 ADRP 167936(PC), R3
0x1000a395c 912f4063 ADD $3024, R3, R3
0x1000a3960 f90047e3 MOVD R3, 136(RSP)
0x1000a3964 3980001b MOVB (R0), R27
0x1000a3968 14000001 JMP 1(PC)
0x1000a396c f90087e0 MOVD R0, 264(RSP)
0x1000a3970 b24003e2 ORR $1, ZR, R2
0x1000a3974 f9008be2 MOVD R2, 272(RSP)
0x1000a3978 f9008fe2 MOVD R2, 280(RSP)
0x1000a397c aa0203e1 MOVD R2, R1
0x1000a3980 97ffed40 CALL fmt.Println(SB)
0x1000a3984 14000001 JMP 1(PC)
var f func() = getFunc()
0x1000a3988 97ffff7e CALL main.getFunc(SB)
0x1000a398c f9001be0 MOVD R0, 48(RSP)
if f == nil {
0x1000a3990 b5000040 CBNZ R0, 2(PC)
0x1000a3994 14000002 JMP 2(PC)
0x1000a3998 14000014 JMP 20(PC)
fmt.Println("f is nil")
0x1000a399c a9087fff STP (ZR, ZR), 128(RSP)
0x1000a39a0 910203e0 ADD $128, RSP, R0
0x1000a39a4 f9002be0 MOVD R0, 80(RSP)
0x1000a39a8 3980001b MOVB (R0), R27
0x1000a39ac 900000c3 ADRP 98304(PC), R3
0x1000a39b0 912d0063 ADD $2880, R3, R3
0x1000a39b4 f90043e3 MOVD R3, 128(RSP)
0x1000a39b8 b0000143 ADRP 167936(PC), R3
0x1000a39bc 912f8063 ADD $3040, R3, R3
0x1000a39c0 f90047e3 MOVD R3, 136(RSP)
0x1000a39c4 3980001b MOVB (R0), R27
0x1000a39c8 14000001 JMP 1(PC)
0x1000a39cc f9007be0 MOVD R0, 240(RSP)
0x1000a39d0 b24003e2 ORR $1, ZR, R2
0x1000a39d4 f9007fe2 MOVD R2, 248(RSP)
0x1000a39d8 f90083e2 MOVD R2, 256(RSP)
0x1000a39dc aa0203e1 MOVD R2, R1
0x1000a39e0 97ffed28 CALL fmt.Println(SB)
0x1000a39e4 14000001 JMP 1(PC)
var i interface{} = getInterface()
0x1000a39e8 97ffff72 CALL main.getInterface(SB)
0x1000a39ec f9003be0 MOVD R0, 112(RSP)
0x1000a39f0 f9003fe1 MOVD R1, 120(RSP)
if i == nil {
0x1000a39f4 b5000040 CBNZ R0, 2(PC)
0x1000a39f8 14000002 JMP 2(PC)
0x1000a39fc 14000014 JMP 20(PC)
fmt.Println("i is nil")
0x1000a3a00 a9087fff STP (ZR, ZR), 128(RSP)
0x1000a3a04 910203e0 ADD $128, RSP, R0
0x1000a3a08 f90027e0 MOVD R0, 72(RSP)
0x1000a3a0c 3980001b MOVB (R0), R27
0x1000a3a10 900000c3 ADRP 98304(PC), R3
0x1000a3a14 912d0063 ADD $2880, R3, R3
0x1000a3a18 f90043e3 MOVD R3, 128(RSP)
0x1000a3a1c b0000143 ADRP 167936(PC), R3
0x1000a3a20 912fc063 ADD $3056, R3, R3
0x1000a3a24 f90047e3 MOVD R3, 136(RSP)
0x1000a3a28 3980001b MOVB (R0), R27
0x1000a3a2c 14000001 JMP 1(PC)
0x1000a3a30 f9006fe0 MOVD R0, 216(RSP)
0x1000a3a34 b24003e2 ORR $1, ZR, R2
0x1000a3a38 f90073e2 MOVD R2, 224(RSP)
0x1000a3a3c f90077e2 MOVD R2, 232(RSP)
0x1000a3a40 aa0203e1 MOVD R2, R1
0x1000a3a44 97ffed0f CALL fmt.Println(SB)
0x1000a3a48 14000001 JMP 1(PC)
从上述汇编代码可以看出,无论是什么类型,其零值判断都是通过第一个字是否为0来判断。具体而言,对于指针类型、map、channel、函数都通过判断其存储的地址是否为0来判断;对于slice,则判断其数据指针是否为0,所以空切片不等于nil;对于interface,则判断其类型指针是否为空指针。
总结
本文总结不同类型nil值的内存结构和nil逻辑判断,其中,err != nil容易出现实际与预期不相符的情况,所以,理解不同类型的nil内存结构和判断方式可以有效避免出现线上问题。