Go Wiki:Go 代码评审意见
本页面收集了 Go 代码评审过程中常见的意见,以便可以通过简写引用单一详细解释。这是一个常见风格问题的清单,并非全面的风格指南。
你可以将此视为 Effective Go 的补充。
与测试相关的其他意见可在 Go Test Comments 找到。
Google 发布了更长的 Go Style Guide。
请在编辑本页面之前讨论变更,即使是细微的修改。许多人有不同意见,这里不是编辑战的地方。
- Gofmt
- 注释句子
- Contexts
- 复制
- 加密随机数
- 声明空切片
- 文档注释
- 不要使用 Panic
- 错误字符串
- 示例
- Goroutine 生命周期
- 处理错误
- 导入
- 空白导入
- 点导入
- 带内错误
- 错误处理流程缩进
- 首字母缩写
- 接口
- 行长度
- 驼峰命名
- 命名结果参数
- 裸返回
- 包注释
- 包名
- 传递值
- 接收器名称
- 接收器类型
- 同步函数
- 有用的测试失败信息
- 变量名
Gofmt
对代码运行 gofmt 可以自动修复大多数机械式风格问题。几乎所有实际使用的 Go 代码都使用了 gofmt
。本文档的其余部分讨论非机械式风格要点。
另一种选择是使用 goimports,它是 gofmt
的超集,还可以根据需要添加(和删除)导入行。
注释句子
参见 https://golang.ac.cn/doc/effective_go#commentary。文档化声明的注释应是完整的句子,即使看起来有点多余。这种方法使得将它们提取到 godoc 文档时格式良好。注释应以描述对象的名称开头,并以句点结尾
// 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` 类型的值在 API 和进程边界上传递安全凭据、跟踪信息、截止时间和取消信号。Go 程序从传入的 RPC 和 HTTP 请求到传出的请求,沿着整个函数调用链显式传递 Contexts。
大多数使用 Context 的函数应将其作为第一个参数接受
func F(ctx context.Context, /* other arguments */) {}
永远不特定于请求的函数可以使用 context.Background(),但即使你认为不需要,也要倾向于传递 Context。默认情况是传递 Context;只有当你有一个充分的理由说明另一种选择是错误时,才直接使用 context.Background()。
不要将 Context 成员添加到结构体类型中;相反,在需要传递 Context 的该类型上的每个方法中添加一个 ctx 参数。唯一的例外是签名必须匹配标准库或第三方库中接口的方法。
不要创建自定义 Context 类型或在函数签名中使用 Context 以外的接口。
如果你有需要传递的应用数据,请将其放在参数中、接收器中、全局变量中,或者,如果它确实属于 Context 值,则放在其中。
Contexts 是不可变的,因此将同一个 ctx 传递给共享相同截止时间、取消信号、凭据、父跟踪等信息的多个调用是没问题的。
复制
为避免意外的别名,复制来自另一个包的结构体时要小心。例如,bytes.Buffer 类型包含一个 []byte
切片。如果你复制一个 Buffer
,副本中的切片可能会与原始切片中的数组别名,导致后续方法调用产生令人惊讶的效果。
一般来说,如果类型 T
的方法与指针类型 *T
相关联,则不要复制 T
类型的值。
加密随机数
不要使用 math/rand
或 math/rand/v2
包来生成密钥,即使是一次性密钥。用 Time.Nanoseconds()
播种,只有很少的熵。相反,使用 crypto/rand.Reader
。如果你需要文本,使用 crypto/rand.Text
,或者,用 encoding/hex
或 encoding/base64
对随机字节进行编码。
import (
"crypto/rand"
"fmt"
)
func Key() string {
return rand.Text()
}
声明空切片
声明空切片时,首选
var t []string
而不是
t := []string{}
前者声明一个 nil 切片值,而后者是非 nil 但长度为零的切片。它们功能上等价——它们的 len
和 cap
都为零——但 nil 切片是首选风格。
请注意,在有限的情况下,非 nil 但长度为零的切片是首选,例如编码 JSON 对象时(nil
切片编码为 null
,而 []string{}
编码为 JSON 数组 []
)。
设计接口时,避免区分 nil 切片和非 nil、长度为零的切片,因为这可能导致细微的编程错误。
关于 Go 中 nil 的更多讨论,请参阅 Francesc Campoy 的演讲 理解 Nil。
文档注释
所有顶级、导出的名称都应有文档注释,非平凡的未导出类型或函数声明也应如此。有关注释约定的更多信息,请参阅 https://golang.ac.cn/doc/effective_go#commentary。
不要使用 Panic
参见 https://golang.ac.cn/doc/effective_go#errors。不要将 panic 用于常规错误处理。使用 error 和多个返回值。
错误字符串
错误字符串不应以大写字母开头(除非以专有名词或首字母缩写词开头),也不应以标点符号结尾,因为它们通常在其他上下文之后打印。也就是说,使用 fmt.Errorf("something bad")
而不是 fmt.Errorf("Something bad")
,这样 log.Printf("Reading %s: %v", filename, err)
格式化时消息中间不会出现不必要的首字母大写。这不适用于日志记录,日志记录隐含地按行处理,不会与其他消息组合。
示例
添加新包时,包括预期用法的示例:可运行的 Example,或一个展示完整调用序列的简单测试。
阅读更多关于可测试的 Example() 函数的信息。
Goroutine 生命周期
当你生成 goroutines 时,要清楚它们何时(或是否)退出。
Goroutines 可能因阻塞在通道发送或接收上而泄漏:即使它阻塞的通道无法访问,垃圾回收器也不会终止 goroutine。
即使 goroutines 不会泄漏,在不再需要时让它们继续运行也可能导致其他细微且难以诊断的问题。向已关闭的通道发送数据会导致 panic。在“结果不再需要”后修改仍在使用的输入仍可能导致数据竞争。让 goroutines 任意长时间运行可能导致不可预测的内存使用。
尽量保持并发代码足够简单,使得 goroutine 的生命周期显而易见。如果确实不可行,请记录 goroutines 何时以及为何退出。
处理错误
参见 https://golang.ac.cn/doc/effective_go#errors。不要使用 _
变量丢弃错误。如果函数返回 error,请检查它以确保函数成功。处理错误,返回它,或者,在真正异常的情况下,使用 panic。
导入
避免重命名导入,除非是为了避免名称冲突;好的包名不应需要重命名。发生冲突时,优先重命名最本地的或项目特定的导入。
导入按组组织,组之间有空行。标准库包总是放在第一组。
package main
import (
"fmt"
"hash/adler32"
"os"
"github.com/foo/bar"
"rsc.io/goversion/version"
)
goimports 会为你做这件事。
空白导入
仅因其副作用而导入的包(使用语法 import _ "pkg"
)应仅在程序的 main 包或需要它们的测试中导入。
点导入
import . 形式在由于循环依赖而无法成为被测试包一部分的测试中可能很有用
package foo_test
import (
"bar/testutil" // also imports "foo"
. "foo"
)
在这种情况下,测试文件不能放在包 foo 中,因为它使用了导入了 foo 的 bar/testutil。因此,我们使用 'import .' 形式让文件假装是包 foo 的一部分,即使它不是。除了这种情况,不要在你的程序中使用 import .。它使得程序更难阅读,因为像 Quux 这样的名称不清楚是当前包还是导入包中的顶级标识符。
带内错误
在 C 及类似语言中,函数通常返回 -1 或 null 等值来表示错误或缺少结果
// Lookup returns the value for key or "" if there is no mapping for key.
func Lookup(key string) string
// Failing to check for an in-band error value can lead to bugs:
Parse(Lookup(key)) // returns "parse failure for value" instead of "no value for key"
Go 对多返回值(multiple return values)的支持提供了一个更好的解决方案。函数不应要求客户端检查带内(in-band)错误值,而应返回一个额外的值来指示其其他返回值是否有效。该返回值可以是 error,或者在不需要解释时可以是 boolean。它应是最终的返回值。
// Lookup returns the value for key or ok=false if there is no mapping for key.
func Lookup(key string) (value string, ok bool)
这可以防止调用者错误地使用结果
Parse(Lookup(key)) // compile-time error
并鼓励编写更健壮和可读的代码
value, ok := Lookup(key)
if !ok {
return fmt.Errorf("no value for %q", key)
}
return Parse(value)
此规则适用于导出的函数,但对于未导出的函数也很有用。
当 nil, “”, 0 和 -1 等返回值是函数的有效结果时,它们是可以接受的,也就是说,调用者不需要将它们与其他值区别对待。
一些标准库函数,例如 “strings” 包中的函数,会返回带内错误值。这大大简化了字符串操作代码,代价是要求程序员更加勤勉。一般来说,Go 代码应为错误返回额外的值。
错误处理流程缩进
尝试将正常代码路径保持在最小缩进,并将错误处理缩进,先处理错误。这通过允许快速视觉扫描正常路径来提高代码的可读性。例如,不要这样写
if err != nil {
// error handling
} else {
// normal code
}
而是这样写
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
如果 if
语句有初始化语句,例如
if x, err := f(); err != nil {
// error handling
return
} else {
// use x
}
那么这可能需要将短变量声明移到自己的行
x, err := f()
if err != nil {
// error handling
return
}
// use x
首字母缩写
名称中是首字母缩写词(initialisms 或 acronyms)(例如 “URL” 或 “NATO”)的单词应保持一致的大小写。例如,“URL” 应显示为 “URL” 或 “url”(如 “urlPony” 或 “URLPony”),绝不能是 “Url”。举个例子:ServeHTTP 而不是 ServeHttp。对于包含多个首字母缩写词“单词”的标识符,例如使用 “xmlHTTPRequest” 或 “XMLHTTPRequest”。
此规则也适用于 “ID”,当它代表“标识符”(identifier)时(几乎所有情况,除非是像 “ego”, “superego” 中的 “id”),所以写 “appID” 而不是 “appId”。
协议缓冲区编译器生成的代码不受此规则约束。人工编写的代码比机器编写的代码有更高的标准。
接口
Go 接口通常属于使用该接口类型值的包,而不是实现这些值的包。实现包应返回具体类型(通常是指针或结构体):这样,可以在不进行大量重构的情况下向实现中添加新方法。
不要在 API 的实现者端定义接口用于“mocking”;相反,设计 API,使其可以使用真实实现的公共 API 进行测试。
不要在接口使用之前定义它们:如果没有实际的使用示例,很难判断接口是否必要,更不用说它应该包含哪些方法了。
package consumer // consumer.go
type Thinger interface { Thing() bool }
func Foo(t Thinger) string { … }
package consumer // consumer_test.go
type fakeThinger struct{ … }
func (t fakeThinger) Thing() bool { … }
…
if Foo(fakeThinger{…}) == "x" { … }
// DO NOT DO IT!!!
package producer
type Thinger interface { Thing() bool }
type defaultThinger struct{ … }
func (t defaultThinger) Thing() bool { … }
func NewThinger() Thinger { return defaultThinger{ … } }
相反,返回一个具体类型,让消费者 mock 生产者实现。
package producer
type Thinger struct{ … }
func (t Thinger) Thing() bool { … }
func NewThinger() Thinger { return Thinger{ … } }
行长度
Go 代码没有严格的行长度限制,但要避免过长的行。同样,如果长行更易读(例如重复性内容),不要为了缩短行而添加换行符。
大多数时候,当人们“不自然地”换行(例如在函数调用或函数声明中间),如果参数数量合理且变量名足够短,这些换行是没有必要的。长行似乎与长名称相关,去除长名称非常有帮助。
换句话说,基于你正在编写内容的语义(作为一般规则)来断行,而不是因为行的长度。如果你发现这导致行太长,那就改变名称或语义,你可能会得到一个好结果。
这实际上与函数长度的建议完全相同。没有“函数永远不能超过 N 行”这样的规则,但确实存在函数过长以及过于重复的微小函数的问题,解决方案是改变函数边界的位置,而不是开始计算行数。
驼峰命名
参见 https://golang.ac.cn/doc/effective_go#mixed-caps。即使这打破了其他语言的约定,此规则也适用。例如,未导出的常量是 maxLength
而不是 MaxLength
或 MAX_LENGTH
。
另请参阅 首字母缩写。
命名结果参数
考虑在 godoc 中会是什么样子。命名结果参数,例如
func (n *Node) Parent1() (node *Node) {}
func (n *Node) Parent2() (node *Node, err error) {}
在 godoc 中会显得重复;最好使用
func (n *Node) Parent1() *Node {}
func (n *Node) Parent2() (*Node, error) {}
另一方面,如果函数返回两个或三个相同类型的参数,或者结果的含义在上下文中不明确,添加名称在某些情况下可能有用。不要仅仅为了避免在函数内部声明变量而命名结果参数;这样做是以不必要的 API 冗余为代价换取了微小的实现简洁性。
func (f *Foo) Location() (float64, float64, error)
不如
// Location returns f's latitude and longitude.
// Negative values mean south and west, respectively.
func (f *Foo) Location() (lat, long float64, err error)
如果函数只有几行,裸返回(Naked returns)是可以接受的。一旦函数变大,请明确指定你的返回值。推论:仅仅因为它能让你使用裸返回而命名结果参数是不值得的。文档的清晰度永远比在函数中节省一两行更重要。
最后,在某些情况下,你需要命名结果参数以便在 deferred closure 中修改它。这总是允许的。
裸返回
不带参数的 return
语句返回命名结果参数的值。这被称为“裸返回” (naked return)。
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
参见 命名结果参数。
包注释
包注释,与所有将在 godoc 中呈现的注释一样,必须紧邻 package 子句,中间没有空行。
// Package math provides basic constants and mathematical functions.
package math
/*
Package template implements data-driven templates for generating textual
output such as HTML.
....
*/
package template
对于“package main”注释,在二进制名称之后可以使用其他风格的注释(如果二进制名称在前面,可以首字母大写),例如,对于目录 seedgen
中的 package main
,你可以写
// Binary seedgen ...
package main
或
// Command seedgen ...
package main
或
// Program seedgen ...
package main
或
// The seedgen command ...
package main
或
// The seedgen program ...
package main
或
// Seedgen ..
package main
这些是示例,其合理的变体是可以接受的。
请注意,以小写字母开头的单词作为包注释的句子开头是不可接受的选项之一,因为这些注释是公开可见的,应以规范的英语书写,包括句子的第一个单词要大写。当二进制名称是第一个单词时,即使它与命令行调用的拼写不完全匹配,也需要将其首字母大写。
有关注释约定的更多信息,请参阅 https://golang.ac.cn/doc/effective_go#commentary。
包名
所有引用你的包中的名称都将使用包名,因此你可以从标识符中省略该名称。例如,如果你在 chubby 包中,你不需要类型 ChubbyFile,客户端会写成 chubby.ChubbyFile
。相反,将类型命名为 File
,客户端会写成 chubby.File
。避免使用 util, common, misc, api, types 和 interfaces 等无意义的包名。更多信息请参阅 https://golang.ac.cn/doc/effective_go#package-names 和 https://golang.ac.cn/blog/package-names。
传递值
不要仅仅为了节省几个字节而将指针作为函数参数传递。如果函数始终只以 *x
的形式引用其参数 x
,那么该参数就不应该是指针。这种情况的常见示例包括传递字符串指针 (*string
) 或接口值指针 (*io.Reader
)。在这两种情况下,值本身是固定大小的,可以直接传递。此建议不适用于大型结构体,甚至可能增长的小型结构体。
接收器名称
方法接收器的名称应反映其身份;通常其类型的单字母或双字母缩写就足够了(例如,“Client” 的“c”或“cl”)。不要使用“me”、“this”或“self”等泛型名称,这些是面向对象语言中赋予方法特殊含义的典型标识符。在 Go 中,方法的接收器只是另一个参数,因此应相应地命名。该名称不必像方法参数那样具有描述性,因为其作用显而易见且没有文档目的。它可以非常短,因为它将出现在该类型几乎每个方法的每一行上;熟悉性允许简洁。也要保持一致:如果你在一个方法中将接收器称为“c”,不要在另一个方法中称其为“cl”。
接收器类型
选择在方法上使用值接收器还是指针接收器可能很困难,特别是对于 Go 新手程序员。如果拿不准,请使用指针,但有时值接收器是合理的,通常出于效率原因,例如对于小型不变的结构体或基本类型的值。一些有用的指导原则
- 如果接收器是 map、func 或 chan,不要使用指向它们的指针。如果接收器是 slice 并且方法不重新切片或重新分配切片,不要使用指向它的指针。
- 如果方法需要改变接收器,接收器必须是指针。
- 如果接收器是包含 sync.Mutex 或类似同步字段的结构体,接收器必须是指针以避免复制。
- 如果接收器是大型结构体或数组,指针接收器更高效。多大算大?假设它等同于将所有元素作为参数传递给方法。如果这感觉太大,那么作为接收器也太大。
- 函数或方法,无论是在并发执行时还是从当前方法调用时,是否可能改变接收器?值类型在方法调用时会创建接收器的副本,因此外部更新不会应用于此接收器。如果变更必须在原始接收器中可见,则接收器必须是指针。
- 如果接收器是结构体、数组或切片,并且其任何元素是指向可能被改变的事物的指针,首选指针接收器,因为这样可以使读者的意图更清晰。
- 如果接收器是一个小的数组或结构体,它本身是值类型(例如 time.Time 类型的东西),没有可变字段也没有指针,或者只是简单的基本类型,如 int 或 string,那么值接收器是合理的。值接收器可以减少垃圾的产生量;如果一个值被传递给一个值方法,可以使用栈上的副本而不是在堆上分配。(编译器试图智能地避免这种分配,但并非总能成功。)不要仅仅因此原因选择值接收器类型,除非先进行性能分析。
- 不要混用接收器类型。为所有可用方法选择指针类型或结构体类型(值类型)。
- 最后,如果不确定,使用指针接收器。
同步函数
优先选择同步函数(直接返回结果或在返回前完成任何回调或通道操作的函数)而不是异步函数。
同步函数将 goroutine 局限在一次调用内部,使其更容易推断它们的生命周期,并避免泄漏和数据竞争。它们也更容易测试:调用者可以传递输入并检查输出,无需轮询或同步。
如果调用者需要更多并发性,他们可以通过从单独的 goroutine 调用函数轻松添加。但在调用者端移除不必要的并发性则相当困难,有时甚至是不可能的。
有用的测试失败信息
测试失败时应提供有用的消息,说明哪里出错了,使用了什么输入,实际得到了什么,以及预期是什么。编写一堆 assertFoo 辅助函数可能很诱人,但请确保你的辅助函数能产生有用的错误消息。假设调试你的失败测试的人不是你,也不是你的团队。一个典型的 Go 测试失败信息如下所示
if got != tt.want {
t.Errorf("Foo(%q) = %d; want %d", tt.in, got, tt.want) // or Fatalf, if test can't test anything more past this point
}
请注意,这里的顺序是 actual != expected,并且消息也使用这个顺序。一些测试框架鼓励反向书写:0 != x,“expected 0, got x”等等。Go 不是这样。
如果这看起来需要很多输入,你可能想写一个表格驱动测试。
使用带有不同输入的测试辅助函数时,另一种区分失败测试的常见技巧是用不同的 TestFoo 函数包装每个调用方,这样测试失败时会显示该名称
func TestSingleValue(t *testing.T) { testHelper(t, []int{80}) }
func TestNoValues(t *testing.T) { testHelper(t, []int{}) }
无论如何,你有责任向将来调试你的代码的人提供有用的失败信息。
变量名
Go 中的变量名应短而不是长。对于作用域有限的局部变量尤其如此。优先使用 c
而不是 lineCount
。优先使用 i
而不是 sliceIndex
。
基本规则是:名称的使用位置离其声明越远,名称就必须越具描述性。对于方法接收器,一两个字母就足够了。循环索引和读取器等常用变量可以是单个字母(i
,r
)。更不寻常的事物和全局变量需要更具描述性的名称。
另请参阅 Google Go 风格指南 中更详细的讨论。
此内容是 Go Wiki 的一部分。