0%

Go 代码风格最佳实践

今天阅读了《Go Code Review Comments》 一文,该文明确了 Go 的代码风格,读后很有收获,在这里记录一些要点。

Gofmt

使用 gofmt 或者 goimports 格式化自己的代码来避免争论。

注释语句

注释应该是完整的语句,即便看起来有些冗余。这样可以在使用 godoc 生成文档时排版更友好。注释应该以要注释的对象开头,以句号结尾。

1
2
3
4
5
// Request represents a request to run a command.
type Request struct { ...

// Encode writes the JSON encoding of req to w.
func Encode(w io.Writer, req *Request) { ...

上下文 Contexts

context.Context 类型携带有认证、trace、deadlines、cancel 信号等信息。在 RPC 和 HTTP 请求中,应该显式地在整个链路中传递 Context。

绝大多数函数的第一个参数应该是 Context:

1
func F(ctx context.Context, /* other arguments */) {}

不要把 Context 作为 struct 的一个属性,而是把 Context 作为 struct method 的第一个参数,有个例外是如果 method 必须
要实现某个接口或者某个第三方 pkg 的签名。

在函数参数中不要使用自定义的 Context 类型、不要使用非 Context 包中定义的接口。

如果有参数需要传递,首先要考虑使用函数参数、reciever、全局变量。如果真的需要的话,才放到 Context 中。

复制

为了避免意想不到的问题,从其他 package 复制 struct 的时候一定要小心。比如说, bytes.Buffer 包含了一个 []byte 的 slice。如果复制了一个 Buffer,你复制出来的 slice 是源数组的别名,调用后续方法时可能会出现意想不到的问题。

通常情况下,一个叫做 T 的 struct,若其 method receiver 是 *T ,不要复制他。

Crypto Rand

不要使用 math/rand 包来生成秘钥,即使是一次性的。在未 Seed 的情况下,生成的结果完全是可预测的。就算使用了 time.Nanoseconds() 来 Seed,它的熵依旧只有几个 bit。取而代之的是,使用 crypto/rand 包的 Reader。如果你需要文本类型的值,将其转换成十六进制或者 Base64。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import (
"crypto/rand"
// "encoding/base64"
// "encoding/hex"
"fmt"
)

func Key() string {
buf := make([]byte, 16)
_, err := rand.Read(buf)
if err != nil {
panic(err) // out of randomness, should never happen
}
return fmt.Sprintf("%x", buf)
// or hex.EncodeToString(buf)
// or base64.StdEncoding.EncodeToString(buf)
}

声明空 Slice

声明空 Slice 时,使用

1
var t []string

而不是

1
t := []string{}

前者声明了一个值为 nil 的 Slice,后者不为 nil 但是长度为 0。它们的功能是一样的——它们的 lencap 都是 0,但是前者依然是推荐的写法。

在几种有限的情况下推荐使用非 nil 但长度为 0 的 Slice(即后一种写法),比如说要 Encode 成 JSON 的时候(nil 会被 Encode 成 null, 而 []string{} 会被 JSON 的 [])。

文档注释

所有顶层的、可导出的名字都应该有注释,其他的类型和函数也应该注释。

不要 Panic

不要使用 panic 来进行正常的错误处理,使用 error 和多返回值。

错误字符串

错误字符串不要以大写字母开头(除非是专有名词和缩写),不要以标点符号结尾。

示例

在添加新的 Package 时,应当包括示例:可以运行的示例,或演示完整调用过程的简单测试。

1
2
3
4
5
6
7
8
9
10
11
12
package stringutil_test

import (
"fmt"

"github.com/golang/example/stringutil"
)

func ExampleReverse() {
fmt.Println(stringutil.Reverse("hello"))
// Output: olleh
}

Goroutine 生命周期

当你开启一个 goroutine 时,弄清楚他们何时退出、是否能够退出。

通过阻塞 channel 的发送或接收可能会引起 goroutines 的内存泄漏:即使被阻塞的 channel 无法访问,垃圾收集器也不会终止 goroutine。

即使 goroutines 没有泄漏,当它们不再需要时却仍然将其留在内存中会导致其他细微且难以诊断的问题。往已经关闭的 channel 发送数据将会引发 panic。在“结果不被需要之后”修改仍在使用的输入仍然可能导致数据竞争。并且将 goroutines 留在内存中任意长时间将会导致不可预测的内存使用。

请尽量让并发代码足够简单,从而更容易地确认 goroutine 的生命周期。如果这不可行,请记录 goroutines 退出的时间和原因。

错误处理

不要使用 _ 来丢弃 error。如果一个函数返回了 error,要检查他以确保函数调用成功。处理 error,返回 error,或者在特定情况下 panic。

Package 导入

避免 Packge 导入时重命名,除非是为了防止名称冲突;好的 Package 名称不需要重命名。如果发生命名冲突,则更倾向于重命名最接近本地的 Package 或特定项目的 Package。

Package 导入按组进行组织,组与组之间有空行。标准库 Package 始终位于第一组中。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import (
"fmt"
"hash/adler32"
"os"

"appengine/foo"
"appengine/user"

"github.com/foo/bar"
"rsc.io/goversion/version"
)

goimports 会为你做这件事。

命名返回参数

想象一下以下示例在 godoc 中的样子。 命名返回参数:

1
2
func (n *Node) Parent1() (node *Node) {}
func (n *Node) Parent2() (node *Node, err error) {}

这样看起来啰啰嗦嗦,最好这样:

1
2
func (n *Node) Parent1() *Node {}
func (n *Node) Parent2() (*Node, error) {}

但是,如果一个函数返回了 2 个或以上的相同类型的参数,或者从上下文中难以清楚地断定返回参数的含义,那么最好给返回参数命名。但是不要仅为了避免参数声明而命名结果参数,这得不偿失。

1
func (f *Foo) Location() (float64, float64, error)

不如下边的代码清晰:

1
2
3
// Location returns f's latitude and longitude.
// Negative values mean south and west, respectively.
func (f *Foo) Location() (lat, long float64, err error)

还有,在某些情况下,你需要命名结果参数,以便在延迟闭包中更改它,这也是可以的。

Package 名字

Package 的所有调用都将使用该 Package 的字来完成,因此可以从标识符中省略该名称。例如,如果有一个 Package 叫做 chubby ,那么不要把类型命名为 ChubbyFile ,否则使用者将写为 chubby.ChubbyFile。而应该将该类型命名为 File,使用时将写为 chubby.File。避免使用无意义的包名称,如 util,common,misc,api,types 和 interfaces。

Reciever 名

一个 method 的 receiver 应该反映其身份,通常情况下 1 到 2 个字母的缩写就可以了(比如说 c、cl 作为 Client 的 receiver)。不要使用通用类的名字诸如 me、this 或者 self 这些。在 Go 中,method 的 receiver 的命名不必像 method 的其他参数一样那么具有描述性,因为他的角色显而易见、不必具有文档性。上下文要保持一致,如果你在一个 method 中使用 c 作为 receiver,不要在另外一个方法中使用 cl

变量名

在 Go 里,变量名应该尽量短而不是长。这一点在有限作用域的局部变量尤为必要。使用 c 而不是 lineCount,使用 i 而不是 sliceIndex

基本规则:作用域越长的变量,其名字越应该更具有描述性。对于方法接收器(method receiver),一个或两个字母就足够了。循环下标、Reader 之类的名字可以是单个字母(i,r)。不常用的变量和全局变量则需要更具描述性的名称。