在日常Golang使用中,你有没有这样的疑惑?
nil是什么?哪些可以用nil?哪些不能用nil?
接下来,我将对这些内容进行总结。
一、什么是nil
首先nil是一个变量,我们可以在源码包中找到这样的描述:
// nil is a predeclared identifier representing the zero value for a
// pointer, channel, func, interface, map, or slice type.
var nil Type // Type must be a pointer, channel, func, interface, map, or slice type
// Type is here for the purposes of documentation only. It is a stand-in
// for any Go type, but represents the same type for any given function
// invocation.
type Type int
从类型定义上可以得到以下关键点:
- nil本质上是一个Type类型的变量
- Type类型仅仅是基于int定义出来的新类型
- nil适用于指针、channel、函数、interface、map、slice六种类型
二、六大类型
1、指针
1.1、变量定义
var ptr *int
1.2、变量本身
变量本身是8字节的内存块
1.3、nil赋值
这8个字节指针置0
1.4、nil判断
判断这8个字节是否为0
2、channel
2.1、变量定义
// 变量本身定义
var c1 chan struct{}
// 变量定义和初始化
var c2 = make(chan struct{})
- 第一种方式仅仅定义了c1变量本身
- 第二种方式则分配了c2的内存,调用了runtime下的makechan函数来创建结构
2.2、变量本身
一个 8 字节的指针而已,指向一个 channel 管理结构,也就是 struct hchan 的指针。
2.3、nil赋值
赋值 nil 之后,仅仅是把这 8 字节的指针置 0 。
2.4、nil判断
判断这指针是否为0
3、map
3.1、变量定义
// 变量定义
var m1 map[int]int
// 变量定义和初始化
var m2 = make(map[int]int)
- 第一种方式仅仅定义了m1变量本身
- 第二种方式则分配了m2的内存,调用了runtime下的makemap函数来创建结构
3.2、变量本身
变量本身是个指针
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
// 键值对的数量
count int // # live cells == size of map. Must be first (used by len() builtin)
// 标识状态
flags uint8
// 2^B = len(buckets)
B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
// 溢出桶里bmap大致数量
noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
// hash因子
hash0 uint32 // hash seed
// 指向一个数组(连续内存空间),数组类型为[]bmap,bmap类型就是存在键值对的结构
buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
// 扩容时,存放之前的buckets
oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
// 分流次数,成倍扩容分流操作计数的字段
nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated)
// 溢出桶结构,正常桶里面某个bmap存满了,会使用这里面的内存空间存放键值对
extra *mapextra // optional fields
}
初始化了map结构后,才能分配map所使用的内存。
3.3、nil赋值
赋值 nil 之后,仅仅是把这 8 字节的指针置 0 。
3.4、nil判断
判断这指针是否为0
4、interface
4.1、变量定义
// 定义一个接口
type Reader interface {
Read(p []byte) (n int, err error)
}
// 定义一个接口变量
var reader Reader
// 空接口
var empty interface{}
4.2、变量本身
type iface struct {
tab *itab
data unsafe.Pointer
}
type eface struct {
_type *_type
data unsafe.Pointer
}
其中iface是通常定义的interface类型,eface是空接口对应的数据结构,这两个结构体占用内存都是16字节。
4.3、nil赋值
赋值 nil 之后,把这 16 字节的内存块置 0 。
4.4、nil判断
需要判断类型和值
5、函数
5.1、变量定义
var f func(int) error
5.2、变量本身
变量本身是8字节的指针
5.3、nil赋值
本身就是指针,只不过指向的是函数而已,所以赋值也是将这 8 字节置 0 。
5.4、nil判断
判断这8个字节是否为0
6、slice
6.1、变量定义
// 定义
var slice1 []int
var slice2 []int = []byte{1,2,3}
// 定义及初始化
var slice3 = make([]int, 3)
var和make这两种方式有什么区别?
- 第一种var的方式定义变量,如果逃逸分析之后,可以确认分配在栈上,那么就在栈上分配24个字节,如果逃逸到堆上,那么调用newobject函数进行类型分析。
- 第二种make方式略有不同,如果逃逸分析之后,确认分配在栈上,那么直接在栈上分配24字节,如果逃逸到堆上,会调用makeslice来分配变量。
6.2、变量本身
type slice struct {
array unsafe.Pointer // 管理的内存块首地址
len int // 动态数组实际使用大小
cap int // 动态数据内存大小
}
变量本身占用24字节。
我们看到无论是var声明定义的slice变量,还是make创建的slice变量,slice管理结构是已经分配出来的,也就是struct slice结构
6.3、nil赋值
本身的 24 字节的内存块被置 0。
6.4、nil判断
那么什么样的slice被认为是nil?
指针为0,也就是这个动态数组没有实际数据的时候。
问题:仅判断指针?对len和cap两个字段不做判断吗?
package main
import "unsafe"
type sliceType struct {
pdata unsafe.Pointer
len int
cap int
}
func main() {
var slice []byte
((*sliceType)(unsafe.Pointer(&slice))).len = 0x3
((*sliceType)(unsafe.Pointer(&slice))).cap = 0x4
if slice != nil {
println("not nil")
} else {
println("nil")
}
}
// Output: nil
三、总结
1、变量就是绑定到某个内存块上的名称
2、变量定义分配的内存是置零分配的
3、不是所有的类型能够赋值 nil,并且和 nil 进行对比判断。只有 指针、channel、函数、interface、map、slice 这 6 种类型
4、channel、map类型的变量需要make才能使用
5、slice在声明后可以使用,是因为struct slice核心结构在定义的时候就已经分配出来了。
6、slice是24字节,interface是16字节,其他的都是8字节
7、这 6 种类型和 nil 进行比较判断本质上都是和变量本身做判断,slice 是判断管理结构的第一个指针字段,map,channel 本身就是指针