Go nil详解
Go 类型的默认值
基本类型(basic type)
- 内置字符串类型:
string
. - 内置布尔类型:
bool
. - 内置数值类型:
int8
、uint8
(byte
)、int16
、uint16
、int32
(rune
)、uint32
、int64
、uint64
、int
、uint
、uintptr
。float32
、float64
。complex64
、complex128
。
注意,byte
是uint8
的一个内置别名,rune
是int32
的一个内置别名。
组合类型(composite type)
Go支持下列组合类型:
- 指针类型- 类C指针
- 结构体类型 - 类C结构体
- 函数类型 - 函数类型在Go中是一种一等公民类别
- 容器类型,包括:
- 数组类型 - 定长容器类型
- 切片类型 - 动态长度和容量容器类型
- 映射类型(
map
)- 也常称为字典类型。在标准编译器中映射是使用哈希表实现的。
- 通道类型- 通道用来同步并发的协程
- 接口类型- 接口在反射和多态中发挥着重要角色
不同类型的默认值
类型 | 默认值 |
---|---|
bool |
false |
numbers |
0 |
string |
"" |
pointer |
nil |
slice |
nil |
map |
nil |
channel |
nil |
function |
nil |
interface |
nil |
值得注意的是 struct
类型和数组类型零值不是nil
。Struct
是各字段值为对应类型的零值,数组类型是各元素类型的零值。且不能将struct
类型和数组类型和与nil`进行等值判断,语法校验不通过。
不同类型变量的声明与初始化
基本类型声明时就会初始化为默认零值。组合类型则分为声明和初始化两阶段。因此只需要讨论组合类型。统一的理解是,只声明未初始化的组合类型变量都是nil
。不过其中还有一些区别,下面一一讲解。
// 声明
var a *int
var b *string
var c []int
var d map[string]string
var e chan int
var f func()
// 初始化
a = make([]int,2)
// 声明+初始化
c := make([]int,2)
d := make(map[string]string)
e := make(chan int)
c := []int{1,2,3,4}
d := map[string]string{"aaa":"bbb"}
c := &[]int{1,2,3,4}
d := &map[string]string{"aaa":"bbb"}
组合类型nil
值
nil pointer
var w *int //对应汇编 MOVQ $0, "".w+48(SP)
// 此时未初始化。 w为nil
if w == nil {
fmt.Println("w is nil")
}
从汇编中可以看到 nil pointer 其实就是指向内存为0的指针。
对指针的操作有两种
- 取值
*w
, 对nil 指针取值会 panic。 - 调用 nil 指针对应的方法,,可以正常调用
nil slice
slice
分为两部分,一部分是slice
本身的结构如下图,其中ptr
指向具体元素。
make([]byte, 0)
make([]byte, 5)
一般都会区分两个概念,nil slice
和 empty slice
nil slice
,其中ptr
声明未初始化,为nil
。 和nil
比较为true
empty slice
,其中ptr
声明并初始化,但是指向空间未分配。和nil
比较为false
var w []int
// 汇编代码
// 设置 ptr 为0
// MOVQ $0, "".w+112(SP)
// XORPS X0, X0
// 设置 len 和 cap 为0
// MOVUPS X0, "".w+120(SP)
w == nil 为 true
w1 := make([]int, 0)
// 汇编代码,功能同上
// MOVQ AX, "".w1+88(SP)
// XORPS X0, X0
// MOVUPS X0, "".w1+96(SP)
w1 == nil 为 false
从汇编代码能清晰的看到,nil slice
和empty slice
都初始化了slice结构,只是ptr的值不同。所以 声明一个nil slice
后 可以直接使用
nil slice
的操作,和empty slice
相同:
可以进行 len
、cap
、 for range
、append
。注意slice
没有删除元素的方法,只能通过在此slice
上新建slice
来完成
// nil slices
var s []int
len(s) // 0
cap(s) // 0
for range s // iterates zero times
s[i] // panic: index out of range
append(s,1) // no eror
nil map
和上述一样,也有两个概念nil map
和empty map
- map声明后初始化前,此时为
nil map
- map声明后不能进行赋值,只有初始化后才能进行赋值操作,初始化后为
empty map
var w map[int]string // nil map
// 汇编
// MOVQ $0, "".w+64(SP)
w == nil 为 true
w1 := map[int]string{} // empty map
// 汇编代码太长,省略
w1 == nil 为 false
从汇编中可以发现,nil map
仅仅声明了一个指针,并置为nil,而empty map
则是声明了对应的数据结构。
因此 nil map
在进行赋值操作前,必须初始化,这是与slice的不同点。
nil map
和empty map
可进行的操作:
nil map
可进行查找(查找任意值会返回数据类型的默认值)、删除、len和range操作,并不会报错 。但是不能进行赋值操作,必须初始化。empty map
, 可进行map所有操作。只声明一个map类型变量时,为
nil map
此时为只读map,无法进行写操作,否则会触发panic
map 构建分两步,
- map声明后初始化前,可进行查找、删除、len和range操作,并不会报错 ,此时为nil map
- map声明后不能进行赋值,只有初始化后才能进行赋值操作,初始化后为empty map
nil map
和empty map
区别:nil map
:只声明未初始化,此时为只读map,不能写入操作,示例:var m map[t]v
empty map
:空map,已初始化,可写入,示例:m := map[t]v{}
或m := make(map[string]string, 0)
// nil maps
var m map[t]u // nil map
m2 := map[t]u{} // empty map
len(m) // 0
for range m // iterates zero times
v, ok := m[i] // zero(u), false
m[i] = x // panic: assignment to entry in nil map
nil channel
var w chan int // nil chan
// 汇编代码
// MOVQ $0, "".w+56(SP)
w1 := make(chan int)
// 汇编代码
// LEAQ type.chan int(SB), AX
// MOVQ AX, (SP)
// MOVQ $0, 8(SP)
// PCDATA $1, $1
// CALL runtime.makechan(SB)
// MOVQ 16(SP), AX
// MOVQ AX, "".w1+48(SP)
从汇编可以看到 channel
也需要声明和初始化。nil chan
仅仅只是创建了一个为nil
指针,初始化才会创建具体的结构体。
nil chan
的操作:
- 读 写 都会无限阻塞
- close 会发生
panic
// nil channels
var c chan t
<- c // blocks forever
c <- x // blocks forever
close(c) // panic: close of nil channel
nil func
map
、channel
、function
的本质都是指向具体实现的指针,而对应类型的nil则是不指向任何地址。因此不做具体介绍
nil interface
interface底层由两部分组成:类型、值(type, value),当二者均为nil时,此时interface才为nil。
var w io.Reader 此时为(nil,nil)
// 汇编
// XORPS X0, X0
// MOVUPS X0, "".w+40(SP)
w == nil // true
var a *strings.Reader 此时为(*strings.Reader,nil)
w = a
// 汇编
MOVQ $0, ""..autotmp_2+32(SP)
LEAQ go.itab.*strings.Reader,io.Reader(SB), AX
MOVQ AX, "".w+40(SP) // _type 为 *strings.Reader
MOVQ $0, "".w+48(SP) // datac 为 nil
w = strings.NewReader("1234") 此时为(*strings.Reader, 对应数据地址)
// 汇编
LEAQ go.string."1234"(SB), AX
MOVQ AX, (SP)
MOVQ $4, 8(SP)
PCDATA $1, $0
CALL strings.NewReader(SB)
MOVQ 16(SP), AX //strings.NewReader(SB) 返回的地址 放入 AX
MOVQ AX, ""..autotmp_1+32(SP)
LEAQ go.itab.*strings.Reader,io.Reader(SB), CX
MOVQ CX, "".w+40(SP) // _type 为 *strings.Reader
MOVQ AX, "".w+48(SP) // datac 为 strings.NewReader(SB) 返回的地址
var s Write // Write 此时 s 为nil,为一个纯接口
var p *Person // nil of type *Person
var s Write = p // 赋值后,s 带有了p的类型信息,但是 p 为nil,所以值为nil
有可得,当interface 为 nil时,变量为一个nil的指针;当不为nil值时,会实例化为下面一种结构体。
type eface struct { // 16 字节 不包含任何方法的接口
_type *_type
data unsafe.Pointer
}
type iface struct { // 16 字节 包含方法的接口
tab *itab
data unsafe.Pointer
}
不要返回具体的错误类型,而应直接返回nil
下面展示返回类型为interface时的差异:
错误示例:
func do() error { // error(*doError, nil)
var err *doError
return err // nil of type *doError
}
func main() {
err := do() // error(*doError, nil)
fmt.Println(err == nil) // false
}
正确示例:
func do() *doError { // nil of type *doError
return nil
}
func main() {
err := do() // nil of type *doError
fmt.Println(err == nil) // true
}
再看下面这段代码,虽然do()
返回nil,但wrapDo()
返回依然是接口,也就是类型为*doError,值为nil
的接口,此时拿到的返回值并不等于nil。
func do() *doError { // nil of type *doError
return nil
}
func wrapDo() error { // error (*doError, nil)
return do() // nil of type *doError
}
func main() {
err := wrapDo() // error (*doError, nil)
fmt.Println(err == nil) // false
}
nil
的有效利用
nil
类型接收者是可以正确调用方法的nil reveivers are userful
Keep nil (pointer) useful if possible, if not NewX()
Use nil slices, they're often fast enough
Use nil maps as read-only empty maps
,将nil map作为只读的空map(不能读nil map进行写入操作,否则会发生panic)Use nil channel to disable a select case
,nil channel来阻塞selct/case语句nil value can satisfy interface
,不同类型的nil值可满足interface,也就是可赋值给interfaceUse nil interface to signal defaul
,使用nil的interface来标识使用缺省处理
nil的一些常见用法
nil pointer
用法
nil pointer
用来和nil比较确认是否为零值
var p *int
p == nil // true
*p // panic: runtime error: invalid memory address or nil pointer dereference
来看看,如何实现二叉树树的求和操作:
type tree {
v int
l *tree
r *tree
}
func (t *tree) Sum() int
第一种方案,有两个问题:
- 代码冗余,重复的
if v != nil {v.m()}
- 当
t
为nil时,会发生panic
var t *tree // nil of type *tree
sum := t.Sum() // panic
// 实现方案1
func (t *tree) Sum() int {
sum := t.v
if t.l != nil {
sum += t.l.Sum()
}
if t.r != nil {
sum += t.r.Sum()
}
return sum
}
nil
接收者,也可以正确调用方法,所以可以利用这一点,改造出方案2:
// 方案2:代码简洁、可断性提高很多
func (t *tree) Sum() int {
if t == nil {
return 0
}
return v + t.l.Sum() + t.r.Sum()
}
通过利用类型的nil,可以灵活实现是否为空的处理,已经便捷实现扩展函数:
func (t *tree) String() string {
if t == nil {
return ""
}
return fmt.Sprint(t.l, t.v, t.r)
}
func (t *tree) Find(v int) bool {
if t == nil {
return false
}
return t.v go== v || t.l.Find(v) || t.r.Find(v)
}
nil slices
用法
- 不能对
nil slice
进行取值,否则会发生panic - 可通过
append
函数对nil slice
进行增加元素操作
var s []int
len(s) // 0
cap(s) // 0
for range s // 执行0次
s[i] // panic: index out of range
for i := 0; i <10; i++ {
fmt.Printf("len: %2d, cap: %2d\n", len(s), cap(s))
s = append(s, i)
}
nil map
用法
nil map
不能进行增加元素操作,因它还没有进行初始化- 将
nil map
作为只读的空map
var m map[t]u
len(m) // 0
for range m // 执行0次
v, ok := m[i] // v=zero(u), ok=false
m[i] = x // panic: assignment to entry in nil map
// 有个nil map有用的例子
func NewGet(url string, headers map[string]string)(*http.Request, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
for k, v := range headers {
req.Header.Set(k, v)
}
return req, nil
}
// 调用时,传递headers
NewGet(
"http://google.com",
map[string]string{
"USER_AGENT":"google/gopher",
}, // go语言五十度灰,如果参数和)之间换行形式,参数尾部需追加,逗号
)
// 调用时,不传递headers,可以传递一个empty map空map
NewGet(
"http://google.com",
map[string]string{}, // 传递空map,empty map
)
// 调用时,不传递headers,可以传递一个nil map
NewGet(
"http://google.com",
nil, // 传递nil map,也是合法的
)
nil channel
用法
- 不能对
nil channel
进行close()
操作,发触发panic: close of nil channel
- 不能对channel进行多次
close
,会触发panic: close of closed channel
- 关闭channel,在select/case中将依然能获取到值,但nil channel将阻塞读操作来失效select/case中逻辑
var c chan t // nil of type chan t
// nil channel操作时
<- c // block forever,持续阻塞
c <- x // block forever,持续阻塞
close(c) // panic: close of nil channel,关闭nil channel发生panic
// 对于已关闭的channel,将发生如下现象
v := <- c //closed channel是可以被消费者继续读取的,在读完了有意义的数据之后,将读到一堆空值。比如这里的int类型就是0。
v, ok := <-c // zero(t), false 不会阻塞,返回零值和False
c <-x // panic: send to close channel
close(x) // panic: close of closed channel,备注原文中错误
现在假设要实现一个合并函数,实现从两个通道中获取数据,然后写入out输出通道:
func merge(out chan<- int, a,b <-chan int) {
for {
select {
case v:= <-a: // 当a/b通道关闭时,这里将持续获取到0
out <-v
case v:= <-b
out <-v
}
}
}
改造代码后如下:
func merge(out chan<- int, a,b <-chan int) {
var aClosed, bClosed bool
for !aClosed || !bClosed {
select {
case v,ok := <-a: // 此时通道关闭后,就不会再进行获取了
if !ok {
aClosed = true
fmt.Println("a is closed")
continue
}
out <-v
case v,ok := <-b:
if !ok {
bClosed = true
fmt.Println("b is closed")
continue
}
out <-v
}
}
close(out) // 需要在不使用后进行close操作
}
终于搞定了,提交代码,转交给测试吧!你会发现“a is closed” 和 “b is closed”可能会执行多次,为什么?因为外部逻辑如果关闭了a/b,此时还能够读取。此时上述代码中会有空转的逻辑,运行下,看看打印输出:
a is closed
a is closed
a is closed
a is closed
a is closed // 很多次无用的空转逻辑
b is closed // 最后一次,a/b都未空时才结束循环
可能看到这里已经有些蒙了,但要明白,无论是否关闭一个chanel,都可以从中读取到值,而如果不需要对channel取值操作了,那么可以将其改为nil,这样会永远阻塞读,防止再发生读操作;同时应在入口增加非nil判断。
func merge(out chan<- int, a, b <-chan int) {
for a != nil || b != nil {
select {
case v, ok := <-a: // 此时通道关闭后,a == nil,在读取会永远阻塞
if !ok {
a = nil
fmt.Println("a is closed")
continue
}
out <- v
case v, ok := <-b:
if !ok {
b = nil
fmt.Println("b is closed")
continue
}
out <- v
}
}
close(out) // 需要再不使用后进行close操作
}
nil func
用法
- go中函数是一等公民
- 函数可以作为struct结构体的字段,缺省值则为nil
type Foo struct {
f func() error // f is type of func() error
}
// 常见用法,传输函数为nil,增加缺省处理
func NewServer(logger func(string, ...interface{})) {
if logger == nil {
logger = log.Printf
}
logger.Printf("initializng %s", os.Getenv("hostname"))
}
nil interface
用法
- 将
nil interface
作为一种信号来使用 - nil指针,不等于nil接口
if err != nil {
...
}
type Summer interface {
Sum() int
}
var t *tree // nil of type *tree
var s Summer = t // nil指针,可以是合法的interface类型的值
// 此时,对接接口类型变量s而言,其类型为*tree,值为nil,也就是说(*tree, nil)行的interface
fmt.Println(t==nil, s.Sum()) // true, 0
type ints []int
func (i ints) Sum() int {
s := 0
for _, v := range i{
s += v
}
return s
}
var i ints
var s Summer = i // summer 为([]int,i)
fmt.Println(s==nil, s.Sum()) // false, 0
// 通过判断接口为nil,来给定缺省值
func doSum(s Summer) int {
if s == nil {
return 0
}
return s.Sum()
}
var t *tree
doSum(t) // interface的类型和值分别为:(*tree, nil)
var i ints
doSum(i) // (ints, nil)
doSum(nil) // (nil, nil)
http.HandleFunc("localhost:8080", nil) // 传递nil,则使用缺省处理