nil理解及empty的使用场景
一、nil基础
- 每个语言基本都有null,java中的null,python中的NULL,之前以为golang中的nil和java的null差不多,但是后来发现其实还是有一些不同的
- 在go文档中,nil是预先声明的标识符,表示pointer,channel,func,interface,map或slice类型的零值。这意思是nil并不是一个关键字,并且只可以用于特定的类型。
这意思是我们可以 nil := “ok”,但是肯定不要这样了 - 声明一个变量,但是不赋值,这个变量默认拥有的是零值:
1
2
3
4
5
6
7
8
9
10bool -> false
numbers -> 0
string -> "" 与Java不一样
pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil
即:pointer、slice、map、channel、function、interface类型的零值是nil
也就是说,可以理解为nil是一种预定义的类型,这个nil可以表示上面这几个类型的零值,也即nil可能有几种不同的类型
后面通过%T来格式化输出,可以知道nil是有不同类型的
但是要注意的是nil是没有默认类型的1
2value := nil // 编译错误:use of untyped nil
// goland 提示 Cannot assign nil without explicit type
- 这里并没有说struct的零值是什么,原因是struct的零值跟其属性有关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15type Album struct {}
type RandomStruct struct {
key string
value int
offline bool
}
func main() {
var a Album
fmt.Println(a) // 输出{}
var rs RandomStruct
fmt.Println(rs) // 输出{ 0 false},因为string的零值是空字符串所以第一个是空
}
也就是说,声明一个struct但是不赋值,其属性默认初始化成对应类型的零值
引用类型和值类型
a. 对于引用类型,其在栈上是一个指针,指向堆上的对象
b. golang中的值类型:int、float、bool、string、array、struct
c. golang中的引用类型:pointer、slice、channel、interface、map、func值传递和引用传递
a. 在函数调用时,golang默认都是按值传递,因此传array、struct时都是传副本,修改函数入参的array、struct不会影响外层调用者的array和struct
b. 至于传指针,实际上也是传了其地址的副本,两个地址指向同一个对象
c. 讲道理golang中没有引用传递,都是传值的,就算函数入参是一个指针ptr2,和外层指针ptr1,地址也是不一样的,只不过ptr1和ptr2都指向了同一个对象,这个和java一样,只不过golang大概率需要显式地传一个指针,java把指针封装了
注意点:map、slice、channel、struct1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80package main
import "fmt"
type Album struct {
key string
value int
offline bool
}
func main() {
a := Album{
key: "key1",
value: 10,
offline: true,
}
fmt.Printf("原始album的内存地址是:%p\n", &a)
fmt.Println(a)
updateAlbum(a)
fmt.Println("updateAlbum后的album:", a)
updateAlbumByPtr(&a)
fmt.Println("updateAlbumByPtr后的album:", a)
fmt.Println("===================================")
m := make(map[string]int)
m["key1"] = 100
fmt.Println(m)
fmt.Printf("原始map的内存地址是:%p\n", m)
updateMap(m)
fmt.Println("updateMap后的map:", m)
fmt.Println("===================================")
s := []int{1, 2, 3}
fmt.Printf("原始slice的内存地址是:%p\n", s)
updateSlice(s)
fmt.Println("updateSlice后的slice:", s)
}
func updateAlbum(a Album) {
fmt.Printf("updateAlbum函数里接收到album的内存地址是:%p\n", &a)
a.key = "updated key 1"
}
func updateAlbumByPtr(a *Album) {
fmt.Printf("updateAlbumByPtr函数里接收到album的内存地址是:%p\n", &a)
a.key = "updated key 2"
}
func updateMap(m map[string]int) {
fmt.Printf("updateMap函数里接收到map的内存地址是:%p\n", &m)
m["key1"] = 50
}
func updateSlice(s []int) {
fmt.Printf("updateSlice函数里接收到slice的内存地址是:%p\n", &s)
s[0] = 60
}
输出:
原始album的内存地址是:0x11066020
{key1 10 true}
updateAlbum函数里接收到album的内存地址是:0x11066050
updateAlbum后的album: {key1 10 true}
updateAlbumByPtr函数里接收到album的内存地址是:0x11068040
updateAlbumByPtr后的album: {updated key 2 10 true}
===================================
map[key1:100]
原始map的内存地址是:0x1107a000
updateMap函数里接收到map的内存地址是:0x11068050
updateMap后的map: map[key1:50]
===================================
原始slice的内存地址是:0x11064054
updateSlice函数里接收到slice的内存地址是:0x110660f0
updateSlice后的slice: [60 2 3]
map和channel不用显式传指针,因为在go源码中都默认是创建的一个指针:
runtime/map.go
runtime/chan.go
至于slice,传的实际上就是其底层数组的地址1
2
3
4
5type slice struct {
array unsafe.Pointer
len int
cap int
}
- 声明和赋值,内存分布
以struct为例,大概了解一下结构体的内存空间1
2
3
4
5
6
7
8声明:声明一个struct,也会分配内存,并且以零值初始化其内存
var p Person // 此时在内存中已经创建了一个结构体空间,存放p,&p的值是内存地址,该结构体的各项属性均为初始值(空串、零值)
创建struct变量:
1)直接声明,var a Album
2)a := Album{},括号中可以直接赋值
3)var a *Album = new(Album),new出来的是指向结构体的指针
4)var a *Album = &Album{}
二、nil的大小、地址、类型
不同的nil类型的占用的内存大小是不一样的,同一个nil类型大小是相同的
对于指针类型,不同的nil指针类型的地址都是一样的,都是0x0,因为nil是预定义的类型1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54package main
import (
"fmt"
"sync"
"unsafe"
)
func main() {
var b bool
var s string
var intPtr *int
var aSlice []int
var aMap map[string]int
var aChannel chan int
var aFunc func(string) int
var err error
var aInterface interface{}
var mutex sync.Mutex
fmt.Println("========================")
fmt.Println(unsafe.Sizeof(b)) // 1
fmt.Println(unsafe.Sizeof(s)) // 8
fmt.Println(unsafe.Sizeof(intPtr)) // 4
fmt.Println(unsafe.Sizeof(aSlice)) // 12
fmt.Println(unsafe.Sizeof(aMap)) // 4
fmt.Println(unsafe.Sizeof(aChannel)) // 4
fmt.Println(unsafe.Sizeof(aFunc)) // 4
fmt.Println(unsafe.Sizeof(err)) // 8
fmt.Println(unsafe.Sizeof(aInterface)) // 8
fmt.Println(unsafe.Sizeof(mutex)) // 8
fmt.Println("========================")
fmt.Printf("%p\n", intPtr) // 0x0
fmt.Printf("%p\n", aSlice) // 0x0
fmt.Printf("%p\n", aMap) // 0x0
fmt.Printf("%p\n", aChannel) // 0x0
fmt.Printf("%p\n", aFunc) // 0x0
fmt.Println("========================")
fmt.Printf("%T\n", b) // bool
fmt.Printf("%T\n", s) // string
fmt.Printf("%T\n", intPtr) // *int
fmt.Printf("%T\n", aSlice) // []int
fmt.Printf("%T\n", aMap) // map[string]int
fmt.Printf("%T\n", aChannel) // chan int
fmt.Printf("%T\n", aFunc) // func(string) int
fmt.Printf("%T\n", err) // <nil>
fmt.Printf("%T\n", aInterface) // <nil>
fmt.Printf("%T\n", mutex) // sync.Mutex
}
三、nil比较
在java中,null值是可以比较的,并且结果是相等的1
2
3public static void main(String[] args) {
System.out.Println(null == null); // true
}
但是go中,nil是具有类型的,不同类型的nil是不可以比较的
nil跟nil不能比较:
1
2fmt.Println(nil == nil)
// 编译错误:invalid operation: nil == nil (operator == not defined on nil)不同类型的nil不能比较,很明显两个类型是不能直接比较的
1
2
3
4var intPtr *int
var array []int
fmt.Println(intPtr == array)
// invalid operation: intPtr == array (mismatched types *int and []int)相同类型的nil 有时不能比较
map、slice和function类型的nil值不能比较1
2
3
4
5
6
7var a *int
var b *int
fmt.Println(a == b) // 可以
var s1 []int
var s2 []int
fmt.Println(s1 == s2) // 不可以
Read More:
- go语言的比较运算 和 Golang中的struct能不能比较,讲的是struct的比较
- reflect.deepEqual()函数
- 自定义equals和hashcode
- struct类型自定义排序,比如根据name字典序排序,或者是先根据age排序再根据name排序
四、注意点
4.1 引用类型的nil和empty
这里联系一下最开始讲的引用类型和值类型,其中map、slice、channel
对于一个nil的slice、map,是可以对其进行遍历的,这个跟java不一样,java中如果遍历一个null的引用类型会NPE。
但是,不能对nil的引用类型进行赋值1
2
3
4
5
6
7
8
9
10
11var nilSlice []int // nilSlice 是nil
for i, v := range nilSlice { // 循环次数为0
fmt.Println(i, ":", v)
}
var nilMap map[string]int // nilMap 是nil
for k, v := range nilMap { // 循环次数为0
fmt.Printf("%s -> %d\n", k, v)
}
fmt.Println(nilMap["key1"]) // 输出0
nilMap["key1"] = 1 // panic: assignment to entry in nil mapnil slice 和 empty slice:
a. 我们知道slice底层引用的是一个数组,可以将slice看成[ pointer, length, capacity ],则:
b. nil slice对应着[ nil, 0, 0 ],底层没有引用一个数组
c. empty slice对应着[ address, 0, 0 ],底层引用了一个数组1
2
3
4
5
6
7
8
9
10
11// go源码 runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
// test code
var slice []int // nil slice
slice := make([]int, 0) // empty slice
slice := []int{} // empty slice这里就要注意一些参数校验的场景,比如判断集合是nil,或者判断集合是否包含元素
注意在go中slice/array统一用len(a)>0来判断即可,不需要再重复判断是否为nil1
2
3
4// java, org.apache.commons.collections4.CollectionUtils
public static boolean isEmpty(final Collection<?> coll) {
return coll == null || coll.isEmpty();
}对于一个nil的array指针,其循环次数是其数组长度,但是如果数组长度不为0,且range遍历的时候不忽略第二个值,则会panic
1
2
3
4
5
6
7
8
9
10
11
12
13
14var nilArrayPtr *[3]int
fmt.Pritnln(nilArrayPtr == nil) // true
for i, _ := range nilArrayPtr {
fmt.Println(i) // 输出0 1 2
}
for i, v := range nilArrayPtr {
fmt.Println(i, v) // panic: runtime error: invalid memory address or nil pointer dereference
}
var nilArrayPtr2 *[0]int
for i, v := range nilArrayPtr2 {
fmt.Println(i, v) // 循环0次,不报错
}
4.2 channel
- channel有三种状态:
a. nil,只声明但没有初始化
b. 正常使用,可读or可写
c. closed,已经关闭了,已经关闭的channel不是nil - close一个nil的channel会panic
- close一个已经close的channel也会panic
- 读或者写一个nil的channel的操作会永远阻塞
- 给一个已经关闭的channel发送数据,引起panic
- 从一个已经关闭的channel接收数据,如果缓冲区中为空,则返回一个零值
- 无缓冲的channel是同步的,而有缓冲的channel是非同步的
4.3 interface
- https://golang.google.cn/doc/faq#nil_error
- https://research.swtch.com/interfaces
- Go语言接口的原理
- Go接口详解
- Dig101-Go 之读懂 interface 的底层设计
- go 接口断言效率
- interface底层实际上可以理解为<type, value>这样一个pair,只有type和value都为nil则这个interface才是nil。文档中提到,只有声明了但没有赋值的interface才是nil interface,只要赋值了,即使赋了一个nil类型,这个interface也不是nil interface了
也就是说,显式地将nil赋值给接口时,接口的type和value都将为nil,此时接口与nil值判断是相等的。但是如果将一个带有类型的nil赋值给接口时,只有value为nil,而type不为nil,此时接口与nil判断将不相等
a. 注意,这个type并不是interface type,而是存储的concrete type,接口类型变量的值不能存储接口变量类型本身的类型
这也解释了为什么interface可以存储任意值理解:
1
2
3var a interface{} // 等价于 var a interface{} = nil
fmt.Println(reflect.TypeOf(a), reflect.ValueOf(a)) // <nil> <invalid reflect.Value>
fmt.Println(a == nil) // ture
a. 第一行var a interface{}相当于 var a interface{} = nil,则a是interface类型的,可以用<type,value>来描述它。前面提到nil是untyped的,则a对应的type是nil;并且把nil这个值赋值给了a,所以a的value也是nil,即a对应的是<nil,nil>。因此a == nil 为true
b. 对invalid.reflect.Value的理解??
1 | var a = (interface{})(nil) |
a. 相当于var a interface{} = (interface{})(nil),则a是interface类型,并且对应着<nil,nil>,a == nil为true
1 | var a interface{} = (*int)(nil) |
a. var b interface{} = (interface{})(nil)代表着b是interface{}类型,且其对应着<interface{},nil>,因此b不为nil
b. var d = (*interface{}})(nil)相当于 var d *interface{} = (*interface{}})(nil),d是有类型的,其类型为interface{},即空接口指针类型,且其值为nil,因此为true,【不是interface{}类型了,而是一个指针类型】
c. 对于int同理
1 | var a *int |
a. 第一行到第三行相当于var b = (interface{})(int),又相当于var b interface{} = (interface{})(\int),则b是一个interface{}类型,对应<*int, nil>,因此不为nil
1 | func main() { |
a. 这是因为将a传入CheckNull方法时,有一个隐含的类型转换,将int类型转换成了interface{}类型,对于CheckNull函数的入参v而言,其对应着<int, nil>,因此v 不为 nil
- nil经常用在判断err上,这里也有值得注意的地方
// TODO
4.4 sync.Mutex
- sync.Mutex是互斥锁,只有Lock和UnLock两个public的方法
- 一般Lock完之后马上defer UnLock
- sync.Mutex的零值表示了未被锁定的互斥量,源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35// A Mutex is a mutual exclusion lock.
// The zero value for a Mutex is an unlocked mutex.
//
// A Mutex must not be copied after first use.
// Mutex是吸纳了Locker接口
type Mutex struct {
state int32
sema uint32
}
type Locker interface {
Lock()
Unlock()
}
const (
mutexLocked = 1 << iota // mutex is locked
mutexWoken
mutexStarving
mutexWaiterShift = iota
starvationThresholdNs = 1e6
}
func (m *Mutex) Lock() {
// Fast path: grab unlocked mutex.
// 通过CAS加锁,如果锁是没有
if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {
if race.Enabled {
race.Acquire(unsafe.Pointer(m))
}
return
}
// Slow path (outlined so that the fast path can be inlined)
m.lockSlow()
}
4.5 sync.RWMutex
- 读写锁,多读单写
- RWMutex的零值表示未加锁状态
1
2
3
4
5
6
7
8
9
10// A RWMutex is a reader/writer mutual exclusion lock.
// The lock can be held by an arbitrary number of readers or a single writer.
// The zero value for a RWMutex is an unlocked mutex.
type RWMutex struct {
w Mutex // held if there are pending writers
writerSem uint32 // semaphore for writers to wait for completing readers
readerSem uint32 // semaphore for readers to wait for completing writers
readerCount int32 // number of pending readers
readerWait int32 // number of departing readers
}
五、nil struct 和 nil interface
1 | package main |
从以上代码可以发现:
- 对于nil的pointer类型(var s *AlbumService),注意这个AlbumService是一个struct
a. 如果该struct的某个方法的receiver是指针类型,那么可以通过一个nil的指针去调用
b. 如果某个方法的receiver是值类型,那么不可以通过nil的指针去调用该方法
c. 不能通过nil的指针去访问struct中的属性 - var s2 AlbumService,此时声明了一个AlbumService结构体类型的变量s2,os已经为其内配内存了,且初始化为零值,因此可以通过s2来调用方法,访问属性(属性为零值)
- struct不能喝nil比较,前面提到,只有特定的类型才可以和nil比较
- 对于nil的接口(var as AnotherService),不能通过它调用方法,只有将这个接口指向其对应的某个实现类才可以调用方法
六、nil的使用场景
6.1 判断方法调用是否有error
// TODO
七、empty的使用场景
7.1 empty struct的使用
- 空struct{}代表不包含任何字段的结构体类型,不占用系统内存,在go源码中,所有空struct都返回相同的地址(Go1.6后有变化】,注意空struct也是可以寻址的
- 在channel中如果不需要传递更多信息,可以使用空struct作为元素类型,由于channel中传递的是副本,用空struct对内存更友好
在对数组去重的场景中,可以借助一个map,map的value可以是空struct类型,借此实现Set
1
2
3type Set struct {
items map[interface{}]struct{}
}
7.2 empty interface的使用
- 所有类型都实现了empty interface,因此empty interface可以用来存储任何类型
- 类型断言,comma,ok判断
- 不能直接把一个其他类型的slice赋值给一个empty interface类型的slice 官网wiki
类似泛型,用于可以接受任何类型的函数,如fmt.Println():
1
2func Println(a ...interface{}) (n int, err error) {...}
func Printf(format string, a ...interface{}) (n int, err error) {...}接口型函数,函数式编程
go源码net/http/server.go中:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
func Handle(pattern string, handler Handler) {
DefaultServeMux.Handle(pattern, handler)
}
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
从这里可以抽取出这样一种编程模式(有点类似适配器模式的感觉):1
2
3
4
5
6
7type I interface {
Method(string) string // 定义入参、返回值
}
func API(I) {...} // 传入实现了接口的对象
func API(func Method(string) string) {...} // 传入与接口中定义的函数签名一样的函数,这样传入的函数的函数名可以与Method的名称不一致
八、nil的优化
其实就是对if/else的优化,以前在java中有一些优化null的手段:
- 工具类校验(Apache Utils、Guava)
- Optional.ofNullable()
- 用map来扭转条件(表驱动)
- 策略模式 + 工厂(map注入)
- …
在go中nil的优化:
- 利用多返回值,第二个返回值返回bool表示是否成功,根据这个来判断而不是判断nil
- //待积累