go 在声明时期就会为变量提供默认值。
下面是所有的原生 go 类型的零值:
- 整数类型:0
- 浮点数:0.0
- 布尔类型:false
- 字符串类型:“”
- 指针,接口,切片,channel,map,function:nil
至于复合类型,比如数组,和结构体的声明过程,就是 递归的,针对于它所有的子元素的初始化
go 奉行零值可用,最经典的案例是切片的 append 过程,即是你最初的切片变量只是声明,并未 make 赋予底层数组,那么 go 系统仍然处理的结果是可用,并非 Panic
var s []int
s = append(s,1)
fmt.Println(s)
下吗再看一个例子
func main() {
var b *bytes.Buffer
fmt.Println(b)
}
fmt.Println(b)
是调用的 b.String()
,那么为什么这里输出的是 <nil>
不是 Panic 呢?
func (b *Buffer) String() string {
if b == nil {
// Special case, useful in debugging.
return "<nil>"
}
return string(b.buf[b.off:])
}
这就是答案,它在实现的 String 中实现了零值可用,原因一:
nil 的对象是完全可以调用属于它身上的方法的,原因二:
这个方法内部实现的时候又是直接 return 一个 <nil>
避免了取 nil 索引值的情况。所以说基于这两点,它零值可用。
再看一个经典的 buffer 例子:
package main
import (
"bytes"
"fmt"
)
func main() {
var a bytes.Buffer
a.Write([]byte("12"))
fmt.Println(a.String())
}
为什么只是声明了 buffer 就可以直接往里面写值呢?
// 结构体
type Buffer struct {
buf []byte // contents are the bytes buf[off : len(buf)]
off int // read at &buf[off], write at &buf[len(buf)]
lastRead readOp // last read operation, so that Unread* can work correctly.
}
// 取自 Write() 相关的代码 这段代码保证了即便是初始化的buf是nil也可以零值可用。
if b.buf == nil && n <= smallBufferSize {
b.buf = make([]byte, n, smallBufferSize)
return 0
}
另外,当切片是 nil 的时候,也是完全可以取它的 [:]
切片的,只要不超过 index 都没问题,这也是满足零值可用的一个小 tips
var s []int
s = s[:] // 或者 s = s[:0] or s = s[0:] or s= s[0:0]
- slice 零值赋值
- map 零值赋值
- 互斥锁的值复制
var s slice
s[0] = 1 // 错误 ❌
var m map[int]int
m[1] = 12 // 错误 ❌
再看互斥锁的案例
var mu sync.Mutex
mu.Lock()
mu.Unlock()
这段代码是可以正常使用的
但是,如果队 mu 进行值的复制就不能使用了
package main
import (
"sync"
)
func main() {
var mu sync.Mutex
mu.Lock()
foo(mu)
mu.Unlock()
}
func foo(mu sync.Mutex) {
mu.Lock()
mu.Unlock()
}
这个问题的解释是这样的:互斥锁是带有状态的,就是说,当你复制的时候本来是 a 的状态,然后复制过去还是 a 的状态,但是这是一个新的对象了按道理应该是初始状态,所以就会出现错误,这也是传说中的重入锁 (go 不支持),因为 go 的互斥锁是带有状态的,所以这种复制的方法就会出现错误。