Go 语言常见的变量声明形式:

  1. var a int32
  2. var s string = "hello"
  3. var i = 13
  4. n := 17
  5. var (
  6. crlf = []byte("\r\n")
  7. colonSpace = []byte(": ")
  8. )

Gopher 们在进行变量声明形式的选择上应该尽量保持项目范围内是一致的。

Go 语言有两类变量:

  • 包级变量(package varible):即在 package 级别可见的变量。如果是导出变量,则该包级变量也可以被视为全局变量;
  • 局部变量(local varible):函数或方法体内声明的变量,仅在函数或方法体内可见。

1. 包级变量的声明形式

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

  1. var a int32 = 17
  2. var f float32 = 3.14
  3. vs.
  4. var a = int32(17)
  5. var f = float32(3.14)

从声明一致性的角度出发,Go 更推荐我们使用后者,这样就统一了接受默认类型和显式指定类型两种声明形式,尤其是在将这些变量放在一个 var 块中声明时,我们更青睐下面这样的形式:

  1. var (
  2. a = 17
  3. f = float32(3.14)
  4. )

而不是下面这种看起来不一致的声明形式:

  1. var (
  2. a = 17
  3. f float32 = 3.14
  4. )

2). 声明但延迟初始化

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

  1. var a int32
  2. var f float64

3). 声明聚类与就近原则

Go 语言提供 var 块用于将多于一个的变量声明放在一起,并且在语法上不会限制放置在 var 块中的声明类型。但是我们一般将同一类的变量声明放在一个 var 块中,不同类的声明放在不同的 var 块中;或者将延迟初始化的变量声明放在一个 var 块,而将声明且显式初始化的变量放在另一个 var 块中。这里我称之为“声明聚类”。比如下面 Go 标准库中的代码:

  1. // $GOROOT/src/net/http/server.go
  2. var (
  3. bufioReaderPool sync.Pool
  4. bufioWriter2kPool sync.Pool
  5. bufioWriter4kPool sync.Pool
  6. )
  7. var copyBufPool = sync.Pool{
  8. New: func() interface{} {
  9. b := make([]byte, 32*1024)
  10. return &b
  11. },
  12. }
  13. ... ...
  14. // $GOROOT/src/net/net.go
  15. var (
  16. // aLongTimeAgo is a non-zero time, far in the past, used for
  17. // immediate cancelation of dials.
  18. aLongTimeAgo = time.Unix(1, 0)
  19. // nonDeadline and noCancel are just zero values for
  20. // readability with functions taking too many parameters.
  21. noDeadline = time.Time{}
  22. noCancel = (chan struct{})(nil)
  23. )
  24. var threadLimit chan struct{}
  25. ... ...

是否将包级变量的声明全部集中放在源文件头部呢?

使用静态编程语言的开发人员都知道,变量声明最佳实践中还有一条:就近原则。即尽可能在靠近第一次使用变量的位置声明该变量。就近原则实际上也是变量的作用域最小化的一种实现手段。在 Go 标准库中我们很容易找到符合就近原则的变量声明的例子:

  1. // $GOROOT/src/net/http/request.go
  2. // ErrNoCookie is returned by Request's Cookie method when a cookie is not found.
  3. var ErrNoCookie = errors.New("http: named cookie not present")
  4. // Cookie returns the named cookie provided in the request or
  5. // ErrNoCookie if not found.
  6. // If multiple cookies match the given name, only one cookie will
  7. // be returned.
  8. func (r *Request) Cookie(name string) (*Cookie, error) {
  9. for _, c := range readCookies(r.Header, name) {
  10. return c, nil
  11. }
  12. return nil, ErrNoCookie
  13. }

2. 局部变量的声明形式

对于延迟初始化的局部变量声明,采用带有 var 关键字的声明形式。

  1. // $GOROOT/src/strings/replace.go
  2. func (r *byteReplacer) Replace(s string) string {
  3. var buf []byte // lazily allocated
  4. for i := 0; i < len(s); i++ {
  5. b := s[i]
  6. if r[b] != b {
  7. if buf == nil {
  8. buf = []byte(s)
  9. }
  10. buf[i] = r[b]
  11. }
  12. }
  13. if buf == nil {
  14. return s
  15. }
  16. return string(buf)
  17. }

对于声明且显式初始化的局部变量,建议使用短变量声明形式。

  1. a := 17
  2. f := 3.14
  3. s := "hello, gopher!"

对于不接受默认类型的变量,我们依然可以使用短变量声明形式,只是在”:=”右侧要做一个显式转型:

  1. a := int32(17)
  2. f := float32(3.14)
  3. s := []byte("hello, gopher!")

尽量在分支控制时应用短变量声明形式。

  1. // $GOROOT/src/strings/strings.go
  2. func LastIndexAny(s, chars string) int {
  3. if chars == "" {
  4. // Avoid scanning all of s.
  5. return -1
  6. }
  7. if len(s) > 8 {
  8. // 作者注:在if条件控制语句中使用短变量声明形式
  9. if as, isASCII := makeASCIISet(chars); isASCII {
  10. for i := len(s) - 1; i >= 0; i-- {
  11. if as.contains(s[i]) {
  12. return i
  13. }
  14. }
  15. return -1
  16. }
  17. }
  18. for i := len(s); i > 0; {
  19. // 作者注:在for循环控制语句中使用短变量声明形式
  20. r, size := utf8.DecodeLastRuneInString(s[:i])
  21. i -= size
  22. for _, c := range chars {
  23. if r == c {
  24. return i
  25. }
  26. }
  27. }
  28. return -1
  29. }
  30. // $GOROOT/src/net/net.go
  31. func (v *Buffers) WriteTo(w io.Writer) (n int64, err error) {
  32. // 作者注:在for循环控制语句中使用短变量声明形式
  33. if wv, ok := w.(buffersWriter); ok {
  34. return wv.writeBuffers(v)
  35. }
  36. // 作者注:在if条件控制语句中使用短变量声明形式
  37. for _, b := range *v {
  38. nb, err := w.Write(b)
  39. n += int64(nb)
  40. if err != nil {
  41. v.consume(n)
  42. return n, err
  43. }
  44. }
  45. v.consume(n)
  46. return n, nil
  47. }

如果你在声明局部变量时遇到适合聚类的应用场景,你也应该毫不犹豫地使用 var 块来声明多于一个的局部变量。比如:

  1. // $GOROOT/src/net/dial.go
  2. func (r *Resolver) resolveAddrList(ctx context.Context, op, network,
  3. addr string, hint Addr) (addrList, error) {
  4. ... ...
  5. var (
  6. tcp *TCPAddr
  7. udp *UDPAddr
  8. ip *IPAddr
  9. wildcard bool
  10. )
  11. ... ...
  12. }
  13. // $GOROOT/src/reflect/type.go
  14. // 这是一个非常长的函数,因此将所有var声明都聚合在函数的开始处了
  15. func StructOf(fields []StructField) Type {
  16. var (
  17. hash = fnv1(0, []byte("struct {")...)
  18. size uintptr
  19. typalign uint8
  20. comparable = true
  21. hashable = true
  22. methods []method
  23. fs = make([]structField, len(fields))
  24. repr = make([]byte, 0, 64)
  25. fset = map[string]struct{}{} // fields' names
  26. hasPtr = false // records whether at least one struct-field is a pointer
  27. hasGCProg = false // records whether a struct-field type has a GCProg
  28. )
  29. ... ...
  30. }

3. 小结

image.png