Go 中的空切片与 nil 切片

技术学习 / 2022-10-15

前言

学习 Go 语言时碰到了一个切片的细节错误:初始化时的空切片和 nil 切片,对于 json 数据的返回值不同

空切片

对于切片来说,如果长度为 0,则为空切片

我们有三种方式可以定义一个切片,第一种是 var,第二种是直接赋值,第三种是 make

// var
var a []int

// 直接赋值
a := []int{}

// make
a := make([]int, 0)

以上三种均为空切片,因为长度为 0,但在 goland 中第二种直接赋值的方式下会划线,如果通过 ide 提供的解决错误方式会将第二种方式转为第一种方式,但这两种方式定义的变量实则是不相等的,对于以上三个变量,我们可以进行如下实验:

// var
var a []string
fmt.Printfln("nil = %t, len = %d, cap = %d", a == nil, len(a), cap(a))

// 直接赋值
a = []string{}
fmt.Printfln("nil = %t, len = %d, cap = %d", a == nil, len(a), cap(a))

// make
a = make([]string, 0)
fmt.Printfln("nil = %t, len = %d, cap = %d", a == nil, len(a), cap(a))

对于以上的代码,结果为:

nil = true, len = 0, cap = 0
nil = false, len = 0, cap = 0
nil = false, len = 0, cap = 0

很显然,第一种和第二种返回的并不一样,第一种方法定义的不仅是个空切片,而且是个 nil 切片

nil 切片

nil 切片代表均为零值,对于切片的底层结构体来说,只有三个变量,分别是指向底层数组的指针,切片长度以及切片容量:

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

对于空切片和 nil 切片来说,最大的不同就在于指向底层数组的指针,用 var 定义的切片是不具有底层数组的,故均为零值,为 nil切片,而用直接赋值的方式进行定义的切片底层数组指针指向后面大括号包含的所有元素的地址,尽管大括号内部为空,但是切片指针不为空,故为空切片

nil 切片的特性

在 json.Marshal 编码时,空切片会被编码成空数组 [],而 nil 切片会被编码成 null

var a []int
json1, _ := json.Marshal(a)

b := []int{}
json2, _ := json.Marshal(b)

c := make([]int, 0)
json3, _ := json.Marshal(c)

fmt.Printf("%s %s %s", json1, json2, json3)

上述代码的输出结果为:

null [] []

在 goland 中,第二种方式会划线,会建议转为第一种方式,但二者返回值是不一样的,如果需要定义一个空切片但非 nil 切片,建议使用 make 方式

nil 切片也可以使用 append 操作,底层容量扩增仍然是按照 2 次幂递增,对于未知容量的切片,定义一个 nil 切片是一个好的选择

总结

当卡莫名其妙的 bug 的时候,看看底层代码的注释也许是个好主意

Go