定义
零切片
零切片是指长度大于 0,且其底层数组元素均已被初始化为该类型“零值”的切片(常见于 make 分配或数组切片化)。
slice1 := make([]int, 3) // 0 0 0
slice2 := make([]*int, 3) // nil nil nil空切片
空切片是指长度和容量均为 0,但底层指针指向一个表示“零字节分配”的特殊固定地址(zerobase)而非 nil 的切片。
slice3 := []int{} // []
slice4 := make([]int, 0) // []nil 切片
nil 切片是指长度、容量均为 0,且底层数组指针为 nil的切片(通常由 var s []int 声明且未初始化)。
var slice5 []int // nil
slice6 := *new([]int) // nil比较
创建切片的方式有很多,他们存在着一些细微的差异。
slice test
package main
import (
"encoding/json"
"fmt"
"unsafe"
)
func printSlice(method string, slice []int) {
bytes, _ := json.Marshal(slice)
var arr = *(*[3]int)(unsafe.Pointer(&slice))
fmt.Printf("%15s: %v, len: %d, cap: %d, address: %p, array.address: %13d, json: %4s, isNil: %t\n",
method, slice, len(slice), cap(slice), &slice, arr[0], string(bytes), slice == nil)
}
func main() {
var slice1 []int
printSlice("[]int", slice1)
var slice2 = *new([]int)
printSlice("*new([]int)", slice2)
var slice3 = []int{}
printSlice("[]int{}", slice3)
var slice4 = make([]int, 0)
printSlice("make([]int, 0)", slice4)
var slice5 = make([]int, 3)
printSlice("make([]int, 3)", slice5)
}执行上门这份代码,你会得到如下的输出。当然其中的 address 每次都是随机的,但有意思的是 []int{} 和 make([]int, 0) 创建出来的切片,他们的 array.address 虽然随机确实相同的。
[]int: [], len: 0, cap: 0, address: 0x140000b6018, array.address: 0, json: null, isNil: true
*new([]int): [], len: 0, cap: 0, address: 0x140000b6060, array.address: 0, json: null, isNil: true
[]int{}: [], len: 0, cap: 0, address: 0x140000b6078, array.address: 4378872992, json: [], isNil: false
make([]int, 0): [], len: 0, cap: 0, address: 0x140000b60c0, array.address: 4378872992, json: [], isNil: false
make([]int, 3): [0 0 0], len: 3, cap: 3, address: 0x140000b6108, array.address: 1374390214704, json: [0,0,0], isNil: false注意,上面几种方式创建出来的切片本身地址并非 nil,所以谈及空或者 nil 的问题并不是说这个变量本身的地址,而是对象中 array 的地址。
底层结构分析

零切片很好理解,就是底层元素全是零值的切片。我们接下来主要看空切片和 nil 切片的区别,以及为什么要这么做。
zerobase
在 /src/runtime/malloc.go 中定义了 zerobase 变量,用于表示所有零字节分配的基地址。
// base address for all 0-byte allocations
var zerobase uintptr到了具体分配内存的时候,当 size == 0,就会返回这个 zerobase 地址。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
if size == 0 {
return unsafe.Pointer(&zerobase)
}
...
}为什么要这么做
Go 保证所有零大小的对象在内存中都有一个合法的、非 nil 的地址,而为了节省空间,它们全部指向这同一个 zerobase 地址。