0

Go 中的 nil 切片

 2 years ago
source link: https://blog.singee.me/2020/09/24/e8cb67835ea44243b136e3cdf8d5ea84/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go 中的空值是一个永远的坑,感觉比价值十亿美金的空指针还难受,本文将尝试比较一下 nil 切片和空切片

TL;DR

空切片和零切片没什么大区别,大胆用吧

认识 nil

首先我们先明确几件关于 nil 的事情

  1. nil 不同于 null(或是 NULL、nullptr),null 通常代表空值、空指针,而 nil 则有所区别

    • nil 不能在基本类型中使用
    • nil 可以表示空指针、映射、切片、函数、通道、接口
    • nil 不是关键字,只是一个特殊的值,并且可以被重新赋值
  2. 在 Go 中,nil 是一个类型不确定值,其对于不同类型有着不同的「类型确定值」

  3. 因为 nil 是一个类型不确定值,因此对于强类型且静态类型且不会从其他语句推导类型的 Go 语言而言,当定义变量时使用 nil 值必须显示的指明类型

    • var a = nila := nil 都是错误的
    • var a = nil; a = 3 在某些语言(如 rust)是正常的,因为编译器可以从上下文推导类型,而在 Go 是错误的
    • var a int = nil

一个切片有如下的定义方式

// 空切片
var emptySlice1 []int // 空切片
var emptySlice2 []int = make([]int, 0) // 零切片
var emptySlice3 []int = []int{} // 零切片
var emptySlice4 []int = *new([]int) // 空切片,与 emptySlice1 基本相同

// 非空切片
var notEmptySlice1 []int = make([]int, 2, 5)
var notEmptySlice2 []SomeStruct = make([]SomeStruct, 2, 5)
var notEmptySlice3 []*SomeStruct = make([]*SomeStruct, 2, 5)

从非空看起

非空切片主要体现的是其底层数据结构

Go 的切片初始化语句是 make([]T, LEN, CAP) ,在初始化时,会生成一个长度为 CAP 的数组,并将数组初始化为零值(对于基本类型初始化为 0、false、空字符串等,对于结构体初始化为空结构体,对于指针、容器等则初始化为 nil)

研究空切片

与 nil 比较、取长度

我们运行一下下面的代码

fmt.Println(emptySlice1 == nil)       // true
fmt.Println(emptySlice2 == nil) // false
fmt.Println(emptySlice3 == nil) // false
fmt.Println(emptySlice4 == nil) // true

fmt.Println(len(emptySlice1) == 0) // true
fmt.Println(len(emptySlice2) == 0) // true
fmt.Println(len(emptySlice3) == 0) // true
fmt.Println(len(emptySlice4) == 0) // true

fmt.Println(cap(emptySlice1) == 0) // true
fmt.Println(cap(emptySlice2) == 0) // true
fmt.Println(cap(emptySlice3) == 0) // true
fmt.Println(cap(emptySlice4) == 0) // true

可以得出以下结论

  • 空切片 = nil,零切片 ≠ nil
  • 无论是空切片还是零切片,对其取 len cap 都不会造成恐慌且值为 0
emptySlice1 = append(emptySlice1, 1, 2, 3)
emptySlice2 = append(emptySlice2, 1, 2, 3)
emptySlice3 = append(emptySlice3, 1, 2, 3)
emptySlice4 = append(emptySlice4, 1, 2, 3)

fmt.Println(emptySlice1) // [1 2 3]
fmt.Println(emptySlice2) // [1 2 3]
fmt.Println(emptySlice3) // [1 2 3]
fmt.Println(emptySlice4) // [1 2 3]

Go 语言和其他语言的一大不同点在于 append 并不一定是修改了原切片值,而可能是将原切片拷贝一份(如果空间不足),其内部原理大概是「先判断是否有容量,没有则复制」,因此无论是空切片还是零切片,都是新开辟了一块空间来存储数据的,也因此,append 运行正常、不会造成运行时恐慌。

我们还可以以此明白,对 nil 调用方法不一定会造成恐慌(除非这个方法内部有对指针解引用的操作)

for-range 遍历值

for i, x := range emptySlice1 {
fmt.Println(i, x)
}

for i, x := range emptySlice2 {
fmt.Println(i, x)
}

for i, x := range emptySlice3 {
fmt.Println(i, x)
}

for i, x := range emptySlice4 {
fmt.Println(i, x)
}

fmt.Println("Done")

不会有输出(除了最后的 Done)、不会产生恐慌,原理和第一个的取 length 相同,因为容量都是 0 所以不会遍历。

Go 的 slice 为不可比较类型,因此其仅可和 nil 进行比较,因此,下面的是正确的

fmt.Println(emptySlice1 == nil)       // true
fmt.Println(emptySlice2 == nil) // false
fmt.Println(emptySlice3 == nil) // false
fmt.Println(emptySlice4 == nil) // true

而下面的一定是编译不通过的

fmt.Println(emptySlice2 == []int{}) 
fmt.Println(emptySlice3 == []int{})

同时,上面说的规则中的 nil 必须是类型不确定值 nil,如果将其赋值给了某个特定类型那么这个类型就是类型确定值了,也无法和切片比较,因此,下面的也是编译不通过的

fmt.Println(emptySlice1 == []int{}) 
fmt.Println(emptySlice4 == []int{})

JSON Marshal & Unmarshal


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK