一、nil基础

  1. 每个语言基本都有null,java中的null,python中的NULL,之前以为golang中的nil和java的null差不多,但是后来发现其实还是有一些不同的
  2. go文档中,nil是预先声明的标识符,表示pointer,channel,func,interface,map或slice类型的零值。这意思是nil并不是一个关键字,并且只可以用于特定的类型。

    这意思是我们可以 nil := “ok”,但是肯定不要这样了
  3. 声明一个变量,但是不赋值,这个变量默认拥有的是零值:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    bool  -> 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
2
value := nil // 编译错误:use of untyped nil
// goland 提示 Cannot assign nil without explicit type

  1. 这里并没有说struct的零值是什么,原因是struct的零值跟其属性有关
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    type 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但是不赋值,其属性默认初始化成对应类型的零值

  1. 引用类型和值类型
    a. 对于引用类型,其在栈上是一个指针,指向堆上的对象
    b. golang中的值类型:int、float、bool、string、array、struct
    c. golang中的引用类型:pointer、slice、channel、interface、map、func

  2. 值传递和引用传递
    a. 在函数调用时,golang默认都是按值传递,因此传array、struct时都是传副本,修改函数入参的array、struct不会影响外层调用者的array和struct
    b. 至于传指针,实际上也是传了其地址的副本,两个地址指向同一个对象
    c. 讲道理golang中没有引用传递,都是传值的,就算函数入参是一个指针ptr2,和外层指针ptr1,地址也是不一样的,只不过ptr1和ptr2都指向了同一个对象,这个和java一样,只不过golang大概率需要显式地传一个指针,java把指针封装了
    注意点:map、slice、channel、struct

    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
    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
    80
    package 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后的mapmap[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
5
type slice struct {
array unsafe.Pointer
len int
cap int
}

  1. 声明和赋值,内存分布
    以struct为例,大概了解一下结构体的内存空间
    1
    2
    3
    4
    5
    6
    7
    8
    声明:声明一个struct,也会分配内存,并且以零值初始化其内存
    var p Person // 此时在内存中已经创建了一个结构体空间,存放p,&p的值是内存地址,该结构体的各项属性均为初始值(空串、零值)

    创建struct变量:
    1)直接声明,var a Album
    2)a := Album{},括号中可以直接赋值
    3var a *Album = new(Album),new出来的是指向结构体的指针
    4var 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
54
package 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
3
public static void main(String[] args) {
System.out.Println(null == null); // true
}

但是go中,nil是具有类型的,不同类型的nil是不可以比较的

  1. nil跟nil不能比较:

    1
    2
    fmt.Println(nil == nil)
    // 编译错误:invalid operation: nil == nil (operator == not defined on nil)
  2. 不同类型的nil不能比较,很明显两个类型是不能直接比较的

    1
    2
    3
    4
    var intPtr *int
    var array []int
    fmt.Println(intPtr == array)
    // invalid operation: intPtr == array (mismatched types *int and []int)
  3. 相同类型的nil 有时不能比较
    map、slice和function类型的nil值不能比较

    1
    2
    3
    4
    5
    6
    7
    var a *int
    var b *int
    fmt.Println(a == b) // 可以

    var s1 []int
    var s2 []int
    fmt.Println(s1 == s2) // 不可以

Read More

  1. go语言的比较运算Golang中的struct能不能比较,讲的是struct的比较
  2. reflect.deepEqual()函数
  3. 自定义equals和hashcode
  4. struct类型自定义排序,比如根据name字典序排序,或者是先根据age排序再根据name排序

四、注意点

4.1 引用类型的nil和empty

这里联系一下最开始讲的引用类型和值类型,其中map、slice、channel

  1. 对于一个nil的slice、map,是可以对其进行遍历的,这个跟java不一样,java中如果遍历一个null的引用类型会NPE。
    但是,不能对nil的引用类型进行赋值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var 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 map
  2. nil 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
  3. 这里就要注意一些参数校验的场景,比如判断集合是nil,或者判断集合是否包含元素
    注意在go中slice/array统一用len(a)>0来判断即可,不需要再重复判断是否为nil

    1
    2
    3
    4
    // java, org.apache.commons.collections4.CollectionUtils
    public static boolean isEmpty(final Collection<?> coll) {
    return coll == null || coll.isEmpty();
    }
  4. 对于一个nil的array指针,其循环次数是其数组长度,但是如果数组长度不为0,且range遍历的时候不忽略第二个值,则会panic

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var 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

  1. channel有三种状态:
    a. nil,只声明但没有初始化
    b. 正常使用,可读or可写
    c. closed,已经关闭了,已经关闭的channel不是nil
  2. close一个nil的channel会panic
  3. close一个已经close的channel也会panic
  4. 读或者写一个nil的channel的操作会永远阻塞
  5. 给一个已经关闭的channel发送数据,引起panic
  6. 从一个已经关闭的channel接收数据,如果缓冲区中为空,则返回一个零值
  7. 无缓冲的channel是同步的,而有缓冲的channel是非同步的

4.3 interface

  1. https://golang.google.cn/doc/faq#nil_error
  2. https://research.swtch.com/interfaces
  3. Go语言接口的原理
  4. Go接口详解
  5. Dig101-Go 之读懂 interface 的底层设计
  6. go 接口断言效率
  7. interface底层实际上可以理解为<type, value>这样一个pair,只有type和value都为nil则这个interface才是nil。文档中提到,只有声明了但没有赋值的interface才是nil interface,只要赋值了,即使赋了一个nil类型,这个interface也不是nil interface了
  8. 也就是说,显式地将nil赋值给接口时,接口的type和value都将为nil,此时接口与nil值判断是相等的。但是如果将一个带有类型的nil赋值给接口时,只有value为nil,而type不为nil,此时接口与nil判断将不相等
    a. 注意,这个type并不是interface type,而是存储的concrete type,接口类型变量的值不能存储接口变量类型本身的类型

    这也解释了为什么interface可以存储任意值

  9. 理解:

    1
    2
    3
    var 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
2
3
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{} = (interface{})(nil),则a是interface类型,并且对应着<nil,nil>,a == nil为true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var a interface{} = (*int)(nil)
var b interface{} = (*interface{})(nil)
fmt.Println(a == nil) // false
fmt.Println(b == nil) // false

var c = (*int)(nil)
var d = (*interface{}})(nil)
fmt.Println(c == nil) // true
fmt.Println(d == nil) // true

fmt.Println(reflect.TypeOf(a), reflect.ValueOf(a)) // *int <nil>
fmt.Println(reflect.TypeOf(b), reflect.ValueOf(b)) // *interface {} <nil>
fmt.Println(reflect.TypeOf(c), reflect.ValueOf(c)) // *int <nil>
fmt.Println(reflect.TypeOf(d), reflect.ValueOf(d)) // *interface {} <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
2
3
4
5
6
7
8
var a *int
fmt.Println(a == nil) // true
fmt.Println((interface{})(a) == nil) // false

可以理解成如下:
var b = (interface{})(a)
fmt.Println(reflect.TypeOf(b), reflect.ValueOf(b)) // *int <nil>
fmt.Println(b == nil) // false

a. 第一行到第三行相当于var b = (interface{})(int),又相当于var b interface{} = (interface{})(\int),则b是一个interface{}类型,对应<*int, nil>,因此不为nil

1
2
3
4
5
6
7
8
9
func main() {
var a *int
fmt.Println(a == nil) // true
fmt.Pritnln(CheckNull(a)) // false
}

func CheckNull(v interface{}) bool {
return v == nil
}

a. 这是因为将a传入CheckNull方法时,有一个隐含的类型转换,将int类型转换成了interface{}类型,对于CheckNull函数的入参v而言,其对应着<int, nil>,因此v 不为 nil

  1. nil经常用在判断err上,这里也有值得注意的地方
    // TODO

4.4 sync.Mutex

  1. sync.Mutex是互斥锁,只有Lock和UnLock两个public的方法
  2. 一般Lock完之后马上defer UnLock
  3. 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

  1. 读写锁,多读单写
  2. 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
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
package main

import (
"context"
"fmt"
)

type AlbumService struct {
ctx context.Context
value int
}

func NewAlbumService(ctx context.Context, value int) *AlbumService {
return &AlbumService{
ctx: ctx,
value: value,
}
}

func (s *AlbumService) SayHello() {
fmt.Println("hello")
}

func (s AlbumService) SayHello2() {
fmt.Println("hello2")
}

type AnotherService interface {
SayHi()
}

type AnotherServiceImpl struct{}

func (as *AnotherServiceImpl) SayHi() {
fmt.Println("another service impl")
}

func main() {
var s *AlbumService
fmt.Println("s == nil?", s == nil) // s == nil? true
s.SayHello() // 正常调用
// s.SayHello2() // NPE
// fmt.Println(s.value) // NPE

var s2 AlbumService
s2.SayHello() // 正常调用
s2.SayHello2() // 正常调用
fmt.Println(s2.value) // 0

fmt.Println("s2 == nil?", s2 == nil) // 编译错误,struct不能和nil比较
// cannot convert nil to type AlbumService
// nil is a predeclared identifier representing the zero value for a pointer, channel, func, interface, map, or slice type.
// Type must be a pointer, channel, func, interface, map, or slice type

s2 = *NewAlbumService(context.Background(), 100)
s2.SayHello() // 正常 hello
s2.SayHello2() // 正常 hello2
fmt.Println(s2.value) // 正常 100

var as AnotherService
fmt.Println(as == nil) // true
// as.SayHi() // NPE
as = &AnotherServiceImpl{}
as.SayHi() // 正常 another service impl
}

从以上代码可以发现:

  1. 对于nil的pointer类型(var s *AlbumService),注意这个AlbumService是一个struct
    a. 如果该struct的某个方法的receiver是指针类型,那么可以通过一个nil的指针去调用
    b. 如果某个方法的receiver是值类型,那么不可以通过nil的指针去调用该方法
    c. 不能通过nil的指针去访问struct中的属性
  2. var s2 AlbumService,此时声明了一个AlbumService结构体类型的变量s2,os已经为其内配内存了,且初始化为零值,因此可以通过s2来调用方法,访问属性(属性为零值)
  3. struct不能喝nil比较,前面提到,只有特定的类型才可以和nil比较
  4. 对于nil的接口(var as AnotherService),不能通过它调用方法,只有将这个接口指向其对应的某个实现类才可以调用方法

六、nil的使用场景

6.1 判断方法调用是否有error

// TODO

七、empty的使用场景

7.1 empty struct的使用

  1. 空struct{}代表不包含任何字段的结构体类型,不占用系统内存,在go源码中,所有空struct都返回相同的地址(Go1.6后有变化】,注意空struct也是可以寻址的
  2. 在channel中如果不需要传递更多信息,可以使用空struct作为元素类型,由于channel中传递的是副本,用空struct对内存更友好
  3. 在对数组去重的场景中,可以借助一个map,map的value可以是空struct类型,借此实现Set

    1
    2
    3
    type Set struct {
    items map[interface{}]struct{}
    }
  4. The empty struct

7.2 empty interface的使用

  1. 所有类型都实现了empty interface,因此empty interface可以用来存储任何类型
  2. 类型断言,comma,ok判断
  3. 不能直接把一个其他类型的slice赋值给一个empty interface类型的slice 官网wiki
  4. 类似泛型,用于可以接受任何类型的函数,如fmt.Println():

    1
    2
    func Println(a ...interface{}) (n int, err error) {...}
    func Printf(format string, a ...interface{}) (n int, err error) {...}
  5. 接口型函数,函数式编程
    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
    22
    type 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
7
type I interface {
Method(string) string // 定义入参、返回值
}

func API(I) {...} // 传入实现了接口的对象

func API(func Method(string) string) {...} // 传入与接口中定义的函数签名一样的函数,这样传入的函数的函数名可以与Method的名称不一致

八、nil的优化

其实就是对if/else的优化,以前在java中有一些优化null的手段:

  1. 工具类校验(Apache Utils、Guava)
  2. Optional.ofNullable()
  3. 用map来扭转条件(表驱动)
  4. 策略模式 + 工厂(map注入)

在go中nil的优化:

  1. 利用多返回值,第二个返回值返回bool表示是否成功,根据这个来判断而不是判断nil
  2. //待积累