8.1 包级变量的声明形式

包级变量只能使用带有var关键字的变量声明形式,但在形式细节上仍有一定的灵活度。我们从声明变量时是否延迟初始化这个角度对包级变量进行一次分类。

1. 声明并同时显式初始化

下面是摘自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
)

2. 声明但延迟初始化

对于声明时并不显式初始化的包级变量,我们使用最基本的声明形式:

var a int32
var f float64

虽然没有显式初始化,但Go语言会让这些变量拥有初始的“零值”。如果是自定义的类型,保证其零值可用是非常必要的,这一点将在后文中详细说明。

3. 声明聚类与就近原则

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。如果一个包级变量在包内部被多处使用,那么这个变量还是放在源文件头部声明比较适合。