nil是什么?

前言

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)   

代码说明:

  1. 指针类型 *struct{} :
csharp 复制代码
var p *struct{} = nil
fmt.Println(unsafe.Sizeof(p)) // 8

在 64 位系统中,指针类型通常占用8个字节,因为它们存储的是内存地址,其nil值为0。

  1. 切片类型 []int
csharp 复制代码
var s []int = nil
fmt.Println(unsafe.Sizeof(s)) // 24

切片在 Go 中是一个结构体,包含指向底层数组的指针、切片的长度和容量。因此,即使切片为 nil,它的大小也是 24 字节(在 64 位系统中,每个指针和整数占用 8 个字节),其nil值每个字段均为0。

  1. 映射类型 map[int]bool
go 复制代码
var m map[int]bool = nil
fmt.Println(unsafe.Sizeof(m)) // 8

映射在 Go 中是一个引用类型,本质上是一个指向哈希表的指针。因此,它的大小为 8 个字节(在 64 位系统中),其nil值为空指针。

  1. 通道类型 chan string
go 复制代码
var c chan string = nil
fmt.Println(unsafe.Sizeof(c)) // 8

通道也是引用类型,其值是一个指向底层数据结构的指针,所以大小为 8 个字节(在 64 位系统中),其nil值为空指针。

  1. 函数类型 func()
csharp 复制代码
var f func() = nil
fmt.Println(unsafe.Sizeof(f)) // 8

函数类型在 Go 中也是一个指针,指向函数的入口地址,因此大小为 8 个字节(在 64 位系统中),其nil值为空指针。

  1. 接口类型 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内存结构和判断方式可以有效避免出现线上问题。

相关推荐
浮尘笔记19 小时前
go-zero使用elasticsearch踩坑记:时间存储和展示问题
大数据·elasticsearch·golang·go
彭岳林19 小时前
err != nil ?
go
杰克逊的黑豹19 小时前
不再迷茫:Rust, Zig, Go 和 C
c++·rust·go
DemonAvenger2 天前
深入剖析 sync.Once:实现原理、应用场景与实战经验
分布式·架构·go
一个热爱生活的普通人3 天前
Go语言中 Mutex 的实现原理
后端·go
孔令飞3 天前
关于 LLMOPS 的一些粗浅思考
人工智能·云原生·go
小戴同学3 天前
实时系统降低延时的利器
后端·性能优化·go
Golang菜鸟4 天前
golang中的组合多态
后端·go
Serverless社区4 天前
函数计算支持热门 MCP Server 一键部署
go