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

相关推荐
HyggeBest11 小时前
Golang 并发原语 Sync Once
后端·go
zhuyasen1 天前
当Go框架拥有“大脑”,Sponge框架集成AI开发项目,从“手写”到一键“生成”业务逻辑代码
后端·go·ai编程
写代码的比利1 天前
Kratos 对接口进行加密转发处理的两个方法
go
chenqianghqu1 天前
goland编译过程加载dll路径时出现失败
go
马里嗷1 天前
Go 1.25 标准库更新
后端·go·github
郭京京2 天前
go语言redis中使用lua脚本
redis·go·lua
心月狐的流火号2 天前
分布式锁技术详解与Go语言实现
分布式·微服务·go
一个热爱生活的普通人2 天前
使用 Makefile 和 Docker 简化你的 Go 服务部署流程
后端·go