



和Python、Ruby等动态脚本语言不同,Go语言沿袭了静态编译型语言的传统: 使用变量之前需要先进行变量的声明 。
这里大致列一下Go语言常见的变量声明形式:
var a int32
var s string = "hello"
var i = 13
n := 17
var (
crlf = []byte("\r\n")
colonSpace = []byte(": ")
)
如果让Go语言的设计者重新设计一次变量声明语法,相信他们很大可能不会再给予Gopher这么大的变量声明灵活性,但目前这一切都无法改变。对于以面向工程著称且以解决规模化问题为目标的Go语言, Gopher在变量声明形式的选择上应尽量保持项目范围内一致 。
Go语言有两类变量。
■包级变量(package variable):在package级别可见的变量。如果是导出变量,则该包级变量也可以被视为全局变量。
■局部变量(local variable):函数或方法体内声明的变量,仅在函数或方法体内可见。
下面来分别说明实现这两类变量在声明形式选择上保持一致性的一些最佳实践。
包级变量只能使用带有var关键字的变量声明形式,但在形式细节上仍有一定的灵活度。我们从声明变量时是否延迟初始化这个角度对包级变量进行一次分类。
下面是摘自Go标准库中的代码(Go 1.12):
// $GOROOT/src/io/pipe.go
var ErrClosedPipe = errors.New("io: read/write on closed pipe")
// $GOROOT/src/io/io.go
var EOF = errors.New("EOF")
var ErrShortWrite = errors.New("short write")
我们看到,对于在声明变量的同时进行显式初始化的这类包级变量,实践中多使用下面的格式:
var variableName = InitExpression
Go编译器会自动根据等号右侧的InitExpression表达式求值的类型确定左侧所声明变量的类型。
如果InitExpression采用的是不带有类型信息的常量表达式,比如下面的语句:
var a = 17 var f = 3.14
则包级变量会被设置为常量表达式的默认类型:以整型值初始化的变量a,Go编译器会将之设置为默认类型int;而以浮点值初始化的变量f,Go编译器会将之设置为默认类型float64。
如果不接受默认类型,而是要显式为包级变量a和f指定类型,那么有以下两种声明方式:
// 第一种 var a int32 = 17 var f float32 = 3.14 // 第二种 var a = int32(17) var f = float32(3.14)
从声明一致性的角度出发,Go语言官方更推荐后者,这样就统一了接受默认类型和显式指定类型两种声明形式。尤其是在将这些变量放在一个var块中声明时,我们更青睐这样的形式:
var ( a = 17 f = float32(3.14) )
而不是下面这种看起来不一致的声明形式:
var ( a = 17 f float32 = 3.14 )
对于声明时并不显式初始化的包级变量,我们使用最基本的声明形式:
var a int32 var f float64
虽然没有显式初始化,但Go语言会让这些变量拥有初始的“零值”。如果是自定义的类型,保证其零值可用是非常必要的,这一点将在后文中详细说明。
Go语言提供var块用于将多个变量声明语句放在一起,并且在语法上不会限制放置在var块中的声明类型。但是我们一般将同一类的变量声明放在一个var块中,将不同类的声明放在不同的var块中;或者将延迟初始化的变量声明放在一个var块,而将声明并显式初始化的变量放在另一个var块中。笔者称之为“声明聚类”。比如下面Go标准库中的代码:
// $GOROOT/src/net/http/server.go
var (
bufioReaderPool sync.Pool
bufioWriter2kPool sync.Pool
bufioWriter4kPool sync.Pool
)
var copyBufPool = sync.Pool {
New: func() interface{} {
b := make([]byte, 32*1024)
return &b
},
}
...
// $GOROOT/src/net/net.go
var (
aLongTimeAgo = time.Unix(1, 0)
noDeadline = time.Time{}
noCancel = (chan struct{})(nil)
)
var threadLimit chan struct{}
...
我们看到在server.go中,copyBufPool变量没有被放入var块中,因为它的声明带有显式初始化,而var块中的变量声明都是延迟初始化的;net.go中的threadLimit被单独放在var块外面,一方面是考虑它是延迟初始化的变量声明,另一方面是考虑threadLimit在含义上与var块中标识时间限制的变量有所不同。
大家可能有一个问题:是否应当将包级变量的声明全部集中放在源文件头部呢?使用静态编程语言的开发人员都知道,变量声明最佳实践中还有一条: 就近原则 ,即尽可能在靠近第一次使用变量的位置声明该变量。就近原则实际上是变量的作用域最小化的一种实现手段。在Go标准库中我们很容易找到符合就近原则的变量声明例子,比如下面这个:
// $GOROOT/src/net/http/request.go
var ErrNoCookie = errors.New("http: named cookie not present")
func (r *Request) Cookie(name string) (*Cookie, error) {
for _, c := range readCookies(r.Header, name) {
return c, nil
}
return nil, ErrNoCookie
}
我们看到在request.go的Cookie方法中使用了ErrNoCookie这个变量,而这个包级变量被就近安排在临近该方法定义的位置进行声明。之所以这么做,可能考虑到的一点是在这个源文件中,仅Cookie方法用到了变量ErrNoCookie。如果一个包级变量在包内部被多处使用,那么这个变量还是放在源文件头部声明比较适合。
有了包级变量的知识做铺垫,我们再来讲解局部变量就容易多了。与包级变量相比,局部变量多了一种短变量声明形式,这也是局部变量采用最多的一种声明形式。下面我们来详细看看。
比如标准库strings包中byteReplacer的方法Replace中的变量buf:
// $GOROOT/src/strings/replace.go
func (r *byteReplacer) Replace(s string) string {
var buf []byte // 延迟分配
for i := 0; i < len(s); i++ {
b := s[i]
if r[b] != b {
if buf == nil {
buf = []byte(s)
}
buf[i] = r[b]
}
}
if buf == nil {
return s
}
return string(buf)
}
另一种常见的采用带var关键字声明形式的变量是error类型的变量err(将error类型变量实例命名为err也是Go的一个惯用法),尤其是当defer后接的闭包函数需要使用err判断函数/方法退出状态时。示例代码如下:
func Foo() {
var err error
defer func() {
if err != nil {
...
}
}()
err = Bar()
...
}
短变量声明形式是局部变量最常用的声明形式,它遍布Go标准库代码。对于接受默认类型的变量,可以使用下面的形式:
a := 17 f := 3.14 s := "hello, gopher!"
对于不接受默认类型的变量,依然可以使用短变量声明形式,只是在“:=”右侧要进行显式转型:
a := int32(17)
f := float32(3.14)
s := []byte("hello, gopher!")
这应该是Go中短变量声明形式应用最广泛的场景了。在编写Go代码时,我们很少单独声明在分支控制语句中使用的变量,而是通过短变量声明形式将其与if、for等融合在一起,就像下面这样:
// $GOROOT/src/net/net.go
func (v *Buffers) WriteTo(w io.Writer) (n int64, err error) {
// 笔者注:在if循环控制语句中使用短变量声明形式
if wv, ok := w.(buffersWriter); ok {
return wv.writeBuffers(v)
}
// 笔者注:在for条件控制语句中使用短变量声明形式
for _, b := range *v {
nb, err := w.Write(b)
n += int64(nb)
if err != nil {
v.consume(n)
return n, err
}
}
v.consume(n)
return n, nil
}
这样的应用方式体现出“就近原则”,让变量的作用域最小化了。
由于良好的函数/方法设计讲究的是“单一职责”,因此每个函数/方法规模都不大,很少需要应用var块来聚类声明局部变量。当然,如果你在声明局部变量时遇到适合聚类的应用场景,你也应该毫不犹豫地使用var块来声明多个局部变量。比如:
// $GOROOT/src/net/dial.go
func (r *Resolver) resolveAddrList(ctx context.Context, op, network,
addr string, hint Addr) (addrList, error) {
...
var (
tcp *TCPAddr
udp *UDPAddr
ip *IPAddr
wildcard bool
)
...
}
或是:
// $GOROOT/src/reflect/type.go
// 笔者注:这是一个非常长的函数,因此将所有var声明都聚合在函数的开始处了
func StructOf(fields []StructField) Type {
var (
hash = fnv1(0, []byte("struct {")...)
size uintptr
typalign uint8
comparable = true
hashable = true
methods []method
fs = make([]structField, len(fields))
repr = make([]byte, 0, 64)
fset = map[string]struct{}{}
hasPtr = false
hasGCProg = false
)
...
}
使用一致的变量声明是Go语言的一个最佳实践,我们用图8-1来对变量声明形式做个形象的小结。
从图8-1中我们看到,要想做好代码中变量声明的一致性,需要明确要声明的变量是包级变量还是局部变量、是否要延迟初始化、是否接受默认类型、是否为分支控制变量,并结合聚类和就近原则。
图8-1 变量声明形式使用决策流程图