11.2 零值可用
我们知道了Go类型的零值,接下来了解可用。Go从诞生以来就一直秉承着尽量保持“零值可用”的理念,来看两个例子。
第一个例子是关于切片的:
var zeroSlice []int zeroSlice = append(zeroSlice, 1) zeroSlice = append(zeroSlice, 2) zeroSlice = append(zeroSlice, 3) fmt.Println(zeroSlice) // 输出:[1 2 3]
我们声明了一个[]int类型的切片zeroSlice,但并没有对其进行显式初始化,这样zeroSlice这个变量就被Go编译器置为零值nil。按传统的思维,对于值为nil的变量,我们要先为其赋上合理的值后才能使用。但由于Go中的切片类型具备零值可用的特性,我们可以直接对其进行append操作,而不会出现引用nil的错误。
第二个例子是通过nil指针调用方法:
// chapter3/sources/call_method_through_nil_pointer.go func main() { var p *net.TCPAddr fmt.Println(p) //输出:<nil> }
我们声明了一个net.TCPAddr的指针变量,但并未对其显式初始化,指针变量p会被Go编译器赋值为nil。在标准输出上输出该变量,fmt.Println会调用p.String()。我们来看看TCPAddr这个类型的String方法实现:
// $GOROOT/src/net/tcpsock.go func (a *TCPAddr) String() string { if a == nil { return "<nil>" } ip := ipEmptyString(a.IP) if a.Zone != "" { return JoinHostPort(ip+"%"+a.Zone, itoa(a.Port)) } return JoinHostPort(ip, itoa(a.Port)) }
我们看到Go标准库在定义TCPAddr类型及其方法时充分考虑了“零值可用”的理念,使得通过值为nil的TCPAddr指针变量依然可以调用String方法。
在Go标准库和运行时代码中还有很多践行“零值可用”理念的好例子,最典型的莫过于sync.Mutex和bytes.Buffer了。
我们先来看看sync.Mutex。在C语言中,要使用线程互斥锁,我们需要这么做:
pthread_mutex_t mutex; // 不能直接使用 // 必须先对mutex进行初始化 pthread_mutex_init(&mutex, NULL); // 然后才能执行lock或unlock pthread_mutex_lock(&mutex); pthread_mutex_unlock(&mutex);
但是在Go语言中,我们只需这么做:
var mu sync.Mutex mu.Lock() mu.Unlock()
Go标准库的设计者很贴心地将sync.Mutex结构体的零值设计为可用状态,让Mutex的调用者可以省略对Mutex的初始化而直接使用Mutex。
Go标准库中的bytes.Buffer亦是如此:
// chapter3/sources/bytes_buffer_write.go func main() { var b bytes.Buffer b.Write([]byte("Effective Go")) fmt.Println(b.String()) // 输出:Effective Go }
可以看到,我们无须对bytes.Buffer类型的变量b进行任何显式初始化,即可直接通过b调用Buffer类型的方法进行写入操作。这是因为bytes.Buffer结构体用于存储数据的字段buf支持零值可用策略的切片类型:
// $GOROOT/src/bytes/buffer.go type Buffer struct { buf []byte off int lastRead readOp }
小结
Go语言零值可用的理念给内置类型、标准库的使用者带来很多便利。不过Go并非所有类型都是零值可用的,并且零值可用也有一定的限制,比如:在append场景下,零值可用的切片类型不能通过下标形式操作数据:
var s []int s[0] = 12 // 报错! s = append(s, 12) // 正确
另外,像map这样的原生类型也没有提供对零值可用的支持:
var m map[string]int m["go"] = 1 // 报错! m1 := make(map[string]int) m1["go"] = 1 // 正确
另外零值可用的类型要注意尽量避免值复制:
var mu sync.Mutex mu1 := mu // 错误: 避免值复制 foo(mu) // 错误: 避免值复制
我们可以通过指针方式传递类似Mutex这样的类型:
var mu sync.Mutex foo(&mu) // 正确
保持与Go一致的理念,给自定义的类型一个合理的零值,并尽量保持自定义类型的零值可用,这样我们的Go代码会更加符合Go语言的惯用法。
[1]https://go-proverbs.github.io/
[2]Go语言规范关于变量默认值的描述:https://tip.golang.org/ref/spec#The_zero_value。