本篇文章主要是来聊聊 Golang 中关于 nil 的使用方式及理解,看看有没有你还不知道的情况呢?
使用 Golang 的朋友都知道,在 Golang 的世界里面,有一个预先声明的标识符 nil
nil 标识符可以作为多种数据结构的零值,通常我们会将 nil 就认为是空的意思,就像 C 语言里面的 NULL 一样
此处说到零值,他其实就是一种数据类型还没有被初始化的时候的默认值,对应着的就是一个零值
例如:
整形的零值,是 0
字符串的零值是 “”
那么布尔类型的零值自然就是 false
零值默认为 nil 的数据结构可多了,有这些:
接下来分别从零值为 nil 的几种数据类型来聊聊的 nil 的那些事
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 作为变量名
在 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 的大小,也与他们自身的底层数据结构有关,对于每一个数据结构的底层细节,可以看到文末的历史文章
我们知道,切片的底层数据结构是,一个指针 ptr,一个 cap 表示切片容量,一个 len 表示切片中已有数据的长度
所以,看到这里,对于理解切片的 nil 为什么占用空间是 24 字节,就明白了吧
对于一个空切片,使用的时候,需要注意不能去取索引对应的值,因为对于一个空切片来说,根本不存在,若访问这一片内存,则会报 panic: index out of range
数组越界
我们对于一个 nil 的切片,可以去取地址,可以取长度,可以取容量,自然也是可以使用 append() 来追加数据的
使用 append 来追加数据就会涉及到扩容,此处就不过多赘述了,详情可以查看关于 slice 的原理介绍
对于 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,我们就需要注意是从这个通道读取数据,还是将数据写入到这个通道中
从 nil 通道中读取数据
例如,若定义一个 channel ,var ch chan int
从 nil 通道中读取数据会阻塞: <- ch
写入数据到 nil 通道
写入数据到 nil 通道会阻塞: ch<-1
关闭一个 nil 通道,会 panic , panic: close of nil channel
当然,此处仅是聊聊关于 nil 涉及到的数据结构,以及简单的注意事项, 对于 channel 的原理和使用,可以查看文末的文章链接
对于指针的零值,我们应该是比较熟悉的了,如果你是从 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 函数传参都是值传递
因此,我们一般开发的时候,会使用传指针的方式,虽然传递指针也是指针的拷贝,可是这样的资源开销会小很多
自然,对于指针和内存的内容,我们之后的文章再细聊
函数零值 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 有了更多的认知了呢?希望能够对你有帮助
感谢阅读,欢迎交流,点个赞,关注一波 再走吧
朋友们,你的支持和鼓励,是我坚持分享,提高质量的动力
技术是开放的,我们的心态,更应是开放的。拥抱变化,向阳而生,努力向前行。
我是阿兵云原生,欢迎点赞关注收藏,下次见~
文中提到的技术点,感兴趣的可以查看这些文章: