目录

Nil的使用场景

在日常Golang使用中,你有没有这样的疑惑?

nil是什么?哪些可以用nil?哪些不能用nil

接下来,我将对这些内容进行总结。

一、什么是nil

首先nil是一个变量,我们可以在源码包中找到这样的描述:

1
2
3
4
5
6
7
8
// 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、函数、interfacemapslice六种类型

二、六大类型

1、指针

1.1、变量定义

1
var ptr *int

1.2、变量本身

变量本身是8字节的内存块

1.3、nil赋值

这8个字节指针置0

1.4、nil判断

判断这8个字节是否为0

2、channel

2.1、变量定义

1
2
3
4
// 变量本身定义
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、变量定义

1
2
3
4
// 变量定义
var m1 map[int]int
// 变量定义和初始化
var m2 = make(map[int]int)
  • 第一种方式仅仅定义了m1变量本身
  • 第二种方式则分配了m2的内存,调用了runtime下的makemap函数来创建结构

3.2、变量本身

变量本身是个指针

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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、变量定义

1
2
3
4
5
6
7
8
// 定义一个接口
type Reader interface {
    Read(p []byte) (n int, err error)
}
// 定义一个接口变量
var reader Reader
// 空接口
var empty interface{}

4.2、变量本身

1
2
3
4
5
6
7
8
9
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、变量定义

1
var f func(int) error

5.2、变量本身

变量本身是8字节的指针

5.3、nil赋值

本身就是指针,只不过指向的是函数而已,所以赋值也是将这 8 字节置 0 。

5.4、nil判断

判断这8个字节是否为0

6、slice

6.1、变量定义

1
2
3
4
5
// 定义
var slice1 []int
var slice2 []int = []byte{1,2,3}
// 定义及初始化
var slice3 = make([]int, 3)

varmake这两种方式有什么区别?

  • 第一种var的方式定义变量,如果逃逸分析之后,可以确认分配在栈上,那么就在栈上分配24个字节,如果逃逸到堆上,那么调用newobject函数进行类型分析。
  • 第二种make方式略有不同,如果逃逸分析之后,确认分配在栈上,那么直接在栈上分配24字节,如果逃逸到堆上,会调用makeslice来分配变量。

6.2、变量本身

1
2
3
4
5
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两个字段不做判断吗?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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、函数、interfacemapslice 这 6 种类型

4、channelmap类型的变量需要make才能使用

5、slice在声明后可以使用,是因为struct slice核心结构在定义的时候就已经分配出来了。

6、slice是24字节,interface是16字节,其他的都是8字节

7、这 6 种类型和 nil 进行比较判断本质上都是和变量本身做判断,slice 是判断管理结构的第一个指针字段,map,channel 本身就是指针