你真的知道 GO 中 nil 代表什么吗?

本篇文章主要是来聊聊 Golang 中关于 nil 的使用方式及理解,看看有没有你还不知道的情况呢?

使用 Golang 的朋友都知道,在 Golang 的世界里面,有一个预先声明的标识符 nil

nil 标识符可以作为多种数据结构的零值,通常我们会将 nil 就认为是空的意思,就像 C 语言里面的 NULL 一样

此处说到零值,他其实就是一种数据类型还没有被初始化的时候的默认值,对应着的就是一个零值

例如:

整形的零值,是 0

字符串的零值是 ""

那么布尔类型的零值自然就是 false

零值默认为 nil 的数据结构可多了,有这些:

  • 函数
  • 指针
  • interface{}
  • Map
  • 切片 slice
  • 通道 channel

接下来分别从零值为 nil 的几种数据类型来聊聊的 nil 的那些事

nil 和 true/false 一样,不是 Golang 的关键字

nil 和 一般我们知道的布尔类型的值(true/false)类似,都不是 Golang 的关键字

我们可以将 nil,true,或者 false 作为变量的名字,并进行赋值和输出

func main() {
   log.SetFlags(log.Lshortfile)
   nil := 123
   true := 111
   false := 222
   log.Printf("nil == %+v,true == %+v,false==%+v", nil,true,false)
}

自然,例如 const 是 Golang 中的关键字,我们就没有办法将 const 作为变量名

nil 占用的空间因不同的数据结构而不同

在 C 语言中,我们知道可以通过 sizeof 去查看指针占用的空间,可能是 4 字节,也有可能是 8 字节,一般来说这是对应着 32 位系统和 64 位系统

例如一个空指针,也是会占用空间的,表示他是一个指针,指针的指向是 NULL

那么对应到 Golang 中,以 nil 作为零值的数据结构,同样有自己所占用的空间,占用空间的大小也是不一样的,Golang 中可以使用 unsafe 包中的 Sizeof 方法来进行查看

func main() {
   log.SetFlags(log.Lshortfile)


   var ptr *int = nil
   log.Println("nil 指针:",unsafe.Sizeof(ptr))

   var in interface{} = nil
   log.Println("nil interface{}:",unsafe.Sizeof(in))

   var mp map[string]string = nil
   log.Println("nil map:",unsafe.Sizeof(mp))

   var sli []int = nil
   log.Println("nil slice:",unsafe.Sizeof(sli))

   var ch chan string = nil
   log.Println("nil channel:",unsafe.Sizeof(ch))

   var fun func() = nil
   log.Println("nil 函数:",unsafe.Sizeof(fun))

}

此处可以看到,对于同一个系统,咱们指针的占用空间 C 语言和 Golang 是一样的(都是 8 字节),对于切片,map 等数据结构 nil 的大小,也与他们自身的底层数据结构有关,对于每一个数据结构的底层细节,可以看到文末的历史文章

切片零值 nil

我们知道,切片的底层数据结构是,一个指针 ptr,一个 cap 表示切片容量,一个 len 表示切片中已有数据的长度

所以,看到这里,对于理解切片的 nil 为什么占用空间是 24 字节,就明白了吧

  • 一个指针占用 8 字节
  • 一个 cap int 类型,占用 8 字节
  • 一个 len int 类型,占用 8 字节

对于一个空切片,使用的时候,需要注意不能去取索引对应的值,因为对于一个空切片来说,根本不存在,若访问这一片内存,则会报 panic: index out of range 数组越界

我们对于一个 nil 的切片,可以去取地址,可以取长度,可以取容量,自然也是可以使用 append() 来追加数据的

使用 append 来追加数据就会涉及到扩容,此处就不过多赘述了,详情可以查看关于 slice 的原理介绍

map 零值 nil

对于 map 也是会存在同样的问题,我们去取 map 中的某一个节点的值的时候

如果 map 之前是经过初始化的,那么我们访问一个不存在的 key 是没有问题的,且我们一般去访问 map 中的值的时候会比较谨慎,例如:

func main() {
   log.SetFlags(log.Lshortfile)

   demoMap := map[int]string{
      1: "xiaoming",
      2: "xiaoxiong",
   }
   value, ok := demoMap[3]
   if ok{
      log.Printf("value == %+v",value)
   }else{
      log.Println("no exsit")
   }

   var demoMap2 map[int]string    // nil
   demoMap2[1] = "hhh"     // panic: assignment to entry in nil map

}

对于访问访问一个 nil 的 map ,不会出现问题,但是如果是去写入数据到某个节点上,那么就会出现 panic: assignment to entry in nil map

Channel 通道零值 nil

对于通道 channel 零值 nil,我们就需要注意是从这个通道读取数据,还是将数据写入到这个通道中

从 nil 通道中读取数据

例如,若定义一个 channel ,var ch chan int

从 nil 通道中读取数据会阻塞: <- ch

写入数据到 nil 通道

写入数据到 nil 通道会阻塞: ch<-1

关闭一个 nil 通道,会 panic , panic: close of nil channel

当然,此处仅是聊聊关于 nil 涉及到的数据结构,以及简单的注意事项, 对于 channel 的原理和使用,可以查看文末的文章链接

指针零值 nil

对于指针的零值,我们应该是比较熟悉的了,如果你是从 C 语言转 Golang 的,那么这就更不在话下了

nil 的指针,异常点和前面说到的切片 slice 类似,当访问空指针上的值的时候,就会出现 panic

var ptr *int
log.Println(*ptr)
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xc0000005 code=0x0 addr=0x0 pc=0x211f79]

自然,仍然和切片类似,对于 nil 的指针,我们可以正常打印指针自己的地址,以及直接打印这个指针指向的值

var ptr *int
log.Println(&ptr)
log.Println(ptr)

所以,一般在操作指针的时候,需要认真仔细,特别是对于新手使用指针,更要万分小心

但是一旦你熟悉了指针,你会爱上他的,由于 Golang 函数传参都是值传递

因此,我们一般开发的时候,会使用传指针的方式,虽然传递指针也是指针的拷贝,可是这样的资源开销会小很多

自然,对于指针和内存的内容,我们之后的文章再细聊

interface{} 零值 nil 和 函数零值 nil

函数零值 nil

对于函数的 nil,我们一般会使用在哪里呢?

例如我们传入的参数是一个函数的时候,具体的实现又需要这个函数去做业务,那么,这个时候我们传入了 nil,自然是会出问题的

func main() {
   testDemo(testFun)
   testDemo(nil)

}
func testFun(str string){
   fmt.Println("str == ",str)
}
func testDemo(fun func(string)){
   fun("hello")
}

所以,对于这种参数是函数的情况,咱们使用之前,需要去校验传入的函数是否是一个 nil,这已经是基本操作了

interface{} 零值 nil

interface{} 的零值,还记得占用多少字节吗?它占用的是 16 个字节

稍微了解 interface{} 的底层数据结构的就知道,他的底层是有一个 type 和一个data(无论是 iface 还是 eface),他俩都是指针,因此此处 nil 的 interface{} 占用 16 个字节

从此处,我们可以看到, interface{} 里面包含 2 个因素,只有当着俩因素都是 nil 的时候,整个 interface{} 才会是 nil

var in interface{}
var ptr  *int
if in == nil{
    // 此处的 in  类型是 nil ,值也是 nil ,因此会进来
}
in = ptr
if in == nil{
    // 此时的 in ,类型是 *int ,值是 nil ,因此不会进来
}

有没有觉得还是挺有趣的呢,本次文章仅讨论关于 nil 的内容,其他的延伸内容可以查看文末的地址

看到这里,有没有对 nil 有了更多的认知了呢?希望能够对你有帮助

感谢阅读,欢迎交流,点个赞,关注一波 再走吧

欢迎点赞,关注,收藏

朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力

技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。

我是阿兵云原生,欢迎点赞关注收藏,下次见~

文中提到的技术点,感兴趣的可以查看这些文章:

posted @ 2023-09-28 23:48  阿兵云原生  阅读(363)  评论(0编辑  收藏  举报