Go 中的 nil 切片
Go 中的空值是一个永远的坑,感觉比价值十亿美金的空指针还难受,本文将尝试比较一下 nil 切片和空切片
TL;DR
空切片和零切片没什么大区别,大胆用吧
认识 nil
首先我们先明确几件关于 nil 的事情
nil 不同于 null(或是 NULL、nullptr),null 通常代表空值、空指针,而 nil 则有所区别
- nil 不能在基本类型中使用
- nil 可以表示空指针、映射、切片、函数、通道、接口
- nil 不是关键字,只是一个特殊的值,并且可以被重新赋值
在 Go 中,nil 是一个类型不确定值,其对于不同类型有着不同的「类型确定值」
因为 nil 是一个类型不确定值,因此对于强类型且静态类型且不会从其他语句推导类型的 Go 语言而言,当定义变量时使用 nil 值必须显示的指明类型
var a = nil
与a := nil
都是错误的var a = nil; a = 3
在某些语言(如 rust)是正常的,因为编译器可以从上下文推导类型,而在 Go 是错误的var a int = nil
定义
一个切片有如下的定义方式
1 | // 空切片 |
从非空看起
非空切片主要体现的是其底层数据结构
Go 的切片初始化语句是 make([]T, LEN, CAP)
,在初始化时,会生成一个长度为 CAP
的数组,并将数组初始化为零值(对于基本类型初始化为 0、false、空字符串等,对于结构体初始化为空结构体,对于指针、容器等则初始化为 nil)
研究空切片
与 nil 比较、取长度
我们运行一下下面的代码
1 | fmt.Println(emptySlice1 == nil) // true |
可以得出以下结论
- 空切片 = nil,零切片 ≠ nil
- 无论是空切片还是零切片,对其取
len
cap
都不会造成恐慌且值为 0
添加值
1 | emptySlice1 = append(emptySlice1, 1, 2, 3) |
Go 语言和其他语言的一大不同点在于 append 并不一定是修改了原切片值,而可能是将原切片拷贝一份(如果空间不足),其内部原理大概是「先判断是否有容量,没有则复制」,因此无论是空切片还是零切片,都是新开辟了一块空间来存储数据的,也因此,append 运行正常、不会造成运行时恐慌。
我们还可以以此明白,对 nil 调用方法不一定会造成恐慌(除非这个方法内部有对指针解引用的操作)
for-range 遍历值
1 | for i, x := range emptySlice1 { |
不会有输出(除了最后的 Done)、不会产生恐慌,原理和第一个的取 length 相同,因为容量都是 0 所以不会遍历。
比较
Go 的 slice 为不可比较类型,因此其仅可和 nil 进行比较,因此,下面的是正确的
1 | fmt.Println(emptySlice1 == nil) // true |
而下面的一定是编译不通过的
1 | fmt.Println(emptySlice2 == []int{}) |
同时,上面说的规则中的 nil
必须是类型不确定值 nil,如果将其赋值给了某个特定类型那么这个类型就是类型确定值了,也无法和切片比较,因此,下面的也是编译不通过的
1 | fmt.Println(emptySlice1 == []int{}) |