Go 语言常见的变量声明形式:
var a int32var s string = "hello"var i = 13n := 17var (crlf = []byte("\r\n")colonSpace = []byte(": "))
Gopher 们在进行变量声明形式的选择上应该尽量保持项目范围内是一致的。
Go 语言有两类变量:
- 包级变量(package varible):即在 package 级别可见的变量。如果是导出变量,则该包级变量也可以被视为全局变量;
- 局部变量(local varible):函数或方法体内声明的变量,仅在函数或方法体内可见。
1. 包级变量的声明形式
1). 声明并同时显式初始化
var a int32 = 17var f float32 = 3.14vs.var a = int32(17)var f = float32(3.14)
从声明一致性的角度出发,Go 更推荐我们使用后者,这样就统一了接受默认类型和显式指定类型两种声明形式,尤其是在将这些变量放在一个 var 块中声明时,我们更青睐下面这样的形式:
var (a = 17f = float32(3.14))
而不是下面这种看起来不一致的声明形式:
var (a = 17f float32 = 3.14)
2). 声明但延迟初始化
对于声明时并不立即显式初始化的包级变量,我们使用最基本的声明形式:
var a int32var f float64
3). 声明聚类与就近原则
Go 语言提供 var 块用于将多于一个的变量声明放在一起,并且在语法上不会限制放置在 var 块中的声明类型。但是我们一般将同一类的变量声明放在一个 var 块中,不同类的声明放在不同的 var 块中;或者将延迟初始化的变量声明放在一个 var 块,而将声明且显式初始化的变量放在另一个 var 块中。这里我称之为“声明聚类”。比如下面 Go 标准库中的代码:
// $GOROOT/src/net/http/server.govar (bufioReaderPool sync.PoolbufioWriter2kPool sync.PoolbufioWriter4kPool sync.Pool)var copyBufPool = sync.Pool{New: func() interface{} {b := make([]byte, 32*1024)return &b},}... ...// $GOROOT/src/net/net.govar (// aLongTimeAgo is a non-zero time, far in the past, used for// immediate cancelation of dials.aLongTimeAgo = time.Unix(1, 0)// nonDeadline and noCancel are just zero values for// readability with functions taking too many parameters.noDeadline = time.Time{}noCancel = (chan struct{})(nil))var threadLimit chan struct{}... ...
是否将包级变量的声明全部集中放在源文件头部呢?
使用静态编程语言的开发人员都知道,变量声明最佳实践中还有一条:就近原则。即尽可能在靠近第一次使用变量的位置声明该变量。就近原则实际上也是变量的作用域最小化的一种实现手段。在 Go 标准库中我们很容易找到符合就近原则的变量声明的例子:
// $GOROOT/src/net/http/request.go// ErrNoCookie is returned by Request's Cookie method when a cookie is not found.var ErrNoCookie = errors.New("http: named cookie not present")// Cookie returns the named cookie provided in the request or// ErrNoCookie if not found.// If multiple cookies match the given name, only one cookie will// be returned.func (r *Request) Cookie(name string) (*Cookie, error) {for _, c := range readCookies(r.Header, name) {return c, nil}return nil, ErrNoCookie}
2. 局部变量的声明形式
对于延迟初始化的局部变量声明,采用带有 var 关键字的声明形式。
// $GOROOT/src/strings/replace.gofunc (r *byteReplacer) Replace(s string) string {var buf []byte // lazily allocatedfor 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)}
对于声明且显式初始化的局部变量,建议使用短变量声明形式。
a := 17f := 3.14s := "hello, gopher!"
对于不接受默认类型的变量,我们依然可以使用短变量声明形式,只是在”:=”右侧要做一个显式转型:
a := int32(17)f := float32(3.14)s := []byte("hello, gopher!")
尽量在分支控制时应用短变量声明形式。
// $GOROOT/src/strings/strings.gofunc LastIndexAny(s, chars string) int {if chars == "" {// Avoid scanning all of s.return -1}if len(s) > 8 {// 作者注:在if条件控制语句中使用短变量声明形式if as, isASCII := makeASCIISet(chars); isASCII {for i := len(s) - 1; i >= 0; i-- {if as.contains(s[i]) {return i}}return -1}}for i := len(s); i > 0; {// 作者注:在for循环控制语句中使用短变量声明形式r, size := utf8.DecodeLastRuneInString(s[:i])i -= sizefor _, c := range chars {if r == c {return i}}}return -1}// $GOROOT/src/net/net.gofunc (v *Buffers) WriteTo(w io.Writer) (n int64, err error) {// 作者注:在for循环控制语句中使用短变量声明形式if wv, ok := w.(buffersWriter); ok {return wv.writeBuffers(v)}// 作者注:在if条件控制语句中使用短变量声明形式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 块来声明多于一个的局部变量。比如:
// $GOROOT/src/net/dial.gofunc (r *Resolver) resolveAddrList(ctx context.Context, op, network,addr string, hint Addr) (addrList, error) {... ...var (tcp *TCPAddrudp *UDPAddrip *IPAddrwildcard bool)... ...}// $GOROOT/src/reflect/type.go// 这是一个非常长的函数,因此将所有var声明都聚合在函数的开始处了func StructOf(fields []StructField) Type {var (hash = fnv1(0, []byte("struct {")...)size uintptrtypalign uint8comparable = truehashable = truemethods []methodfs = make([]structField, len(fields))repr = make([]byte, 0, 64)fset = map[string]struct{}{} // fields' nameshasPtr = false // records whether at least one struct-field is a pointerhasGCProg = false // records whether a struct-field type has a GCProg)... ...}
3. 小结

