Go Wiki: Go 代码审查评论
本页面收集了 Go 代码审查中常见的评论,以便可以通过简短的引用来参考详细的解释。这是一个常见的风格问题的列表,而不是一个全面的风格指南。
您可以将此视为 Effective Go 的补充。
有关测试的其他评论可以在 Go 测试评论 中找到。
Google 发布了更长的 Go 风格指南。
请在编辑本页面之前 讨论更改,即使是细微的更改。许多人都有自己的看法,这里不适合进行编辑战。
- Gofmt
- 评论语句
- 上下文
- 复制
- 加密随机数
- 声明空切片
- 文档注释
- 不要恐慌
- 错误字符串
- 示例
- Goroutine 生命周期
- 处理错误
- 导入
- 导入空白
- 导入点
- 带内错误
- 缩进错误流
- 首字母缩略词
- 接口
- 行长度
- 混合大小写
- 命名结果参数
- 裸返回
- 包注释
- 包名
- 传递值
- 接收者名称
- 接收者类型
- 同步函数
- 有用的测试失败
- 变量名
Gofmt
运行 gofmt 来自动修复大部分机械风格问题。几乎所有 Go 代码都使用 gofmt
。本文档的其余部分处理非机械风格的问题。
另一种选择是使用 goimports,它是 gofmt
的超集,还可以根据需要添加(和删除)导入行。
评论语句
请参阅 https://go-lang.org.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) { ...
依此类推。
上下文
context.Context
类型的值在 API 和进程边界之间传递安全凭据、跟踪信息、截止日期和取消信号。Go 程序通过完整的函数调用链显式地传递 Context,从传入的 RPC 和 HTTP 请求到传出的请求。
大多数使用 Context 的函数应将其作为第一个参数接收。
func F(ctx context.Context, /* other arguments */) {}
一个永远不会特定于请求的函数可以使用 context.Background()
,但即使您认为不需要,也要倾向于传递一个 Context。默认情况下是传递 Context;只有当您有充分的理由证明其他方法是错误的,才直接使用 context.Background()
。
不要向结构体类型添加 Context 成员;而是向该类型上需要传递它的每个方法添加 ctx 参数。唯一的例外是那些签名必须匹配标准库或第三方库中的接口的方法。
不要创建自定义 Context 类型或在函数签名中使用除 Context 之外的接口。
如果您有应用程序数据需要传递,请将其放入参数、接收者、全局变量,或者(如果它确实属于那里)放入 Context 值中。
Context 是不可变的,因此将相同的 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、零长度切片,因为这可能导致微妙的编程错误。
有关 nil 在 Go 中的更多讨论,请参阅 Francesc Campoy 的演讲 Understanding Nil。
文档注释
所有顶层、导出的名称都应有文档注释,同样,非平凡的未导出类型或函数声明也应如此。有关注释约定的更多信息,请参阅 https://go-lang.org.cn/doc/effective_go#commentary。
不要恐慌
请参阅 https://go-lang.org.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 生命周期
当你生成 goroutine 时,要清楚它们何时 - 或是否 - 退出。
Goroutine 可能因为阻塞在 channel 发送或接收上而泄露:垃圾收集器不会终止 goroutine,即使它们阻塞的 channel 是不可达的。
即使 goroutine 不泄露,在它们不再需要时让它们处于运行状态也会导致其他微妙且难以诊断的问题。发送到已关闭的 channel 会引发 panic。在“不再需要结果后”修改仍在使用的输入仍然可能导致数据竞争。并且让 goroutine 无限期地处于运行状态可能导致不可预测的内存使用。
尽量保持并发代码足够简单,以便 goroutine 的生命周期显而易见。如果这不可行,请记录 goroutine 何时以及为何退出。
处理错误
请参阅 https://go-lang.org.cn/doc/effective_go#errors。不要使用 _
变量丢弃错误。如果函数返回错误,请检查它以确保函数成功。处理错误、返回错误,或者在真正特殊的情况下,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 中,因为它使用了 bar/testutil,而 bar/testutil 导入了 foo。因此,我们使用“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 对多返回值ir 的支持提供了更好的解决方案。函数不应要求客户端检查带内错误值,而应返回一个附加值来指示其其他返回值是否有效。此返回值可以是 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
首字母缩略词
名称中是首字母缩略词或缩写的词(例如“URL”或“NATO”)具有一致的大小写。例如,“URL”应显示为“URL”或“url”(如“urlPony”或“URLPony”),绝不能是“Url”。例如:ServeHTTP 而不是 ServeHttp。对于具有多个已初始化“词”的标识符,例如使用“xmlHTTPRequest”或“XMLHTTPRequest”。
此规则也适用于“ID”,当它缩写为“identifier”时(几乎所有情况都不是“ego”、“superego”中的“id”),所以写“appID”而不是“appId”。
协议缓冲区编译器生成的代码不受此规则的约束。人类编写的代码比机器编写的代码要求更高。
接口
Go 接口通常属于使用接口类型值的包,而不是实现这些值的包。实现包应返回具体的(通常是指针或结构体)类型:这样,就可以向实现添加新方法,而无需进行大量重构。
不要在 API 的实现方定义接口“用于模拟”;相反,设计 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{ … } }
而是返回一个具体类型,让消费者模拟生产者实现。
package producer
type Thinger struct{ … }
func (t Thinger) Thing() bool { … }
func NewThinger() Thinger { return Thinger{ … } }
行长度
Go 代码没有严格的行长度限制,但要避免不舒服的长行。同样,当长行更具可读性时(例如,当它们是重复的时),不要为了使行变短而添加换行符。
大多数情况下,当人们“不自然地”换行时(大致在函数调用或函数声明的中间,尽管有一些例外),如果他们有合理的参数数量和合理的短变量名,就不会需要换行。长行似乎与长名称有关,摆脱长名称大有帮助。
换句话说,根据您所写内容的语义换行(作为一般规则),而不是根据行的长度。如果您发现这会产生过长的行,那么就更改名称或语义,您很可能会得到一个好的结果。
这实际上与关于函数应该有多长的建议完全相同。没有“函数永远不能超过 N 行”的规则,但确实存在过长的函数和重复的短函数,解决方案是改变函数的边界,而不是开始数行。
混合大小写
请参阅 https://go-lang.org.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) {}
另一方面,如果一个函数返回两个或三个相同类型的参数,或者结果的含义从上下文中不清楚,那么在某些情况下添加名称可能有用。不要仅仅为了避免在函数内部声明一个 var 而命名结果参数;这会牺牲一点实现上的简洁性,但代价是 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)
裸返回对于几行代码的函数是可以的。一旦函数变得中等大小,就要明确你的返回值。推论:仅仅因为命名结果参数就能让你使用裸返回,这并不值得。文档的清晰度总是比节省一两行函数代码更重要。
最后,在某些情况下,你需要命名一个结果参数,以便在延迟的闭包中更改它。这总是可以的。
裸返回
一个不带参数的 return
语句返回命名的返回参数。这被称为“裸”返回。
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
请参阅 命名结果参数。
包注释
包注释,与所有将由 godoc 显示的注释一样,必须紧邻包子句出现,中间没有空行。
// 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://go-lang.org.cn/doc/effective_go#commentary。
包名
所有对包中名称的引用都将使用包名完成,因此您可以从标识符中省略该名称。例如,如果您在包 chubby 中,则不需要类型 ChubbyFile,客户端会写成 chubby.ChubbyFile
。而是命名类型 File
,客户端会写成 chubby.File
。避免无意义的包名,如 util、common、misc、api、types 和 interfaces。有关更多信息,请参阅 https://go-lang.org.cn/doc/effective_go#package-names 和 https://go-lang.org.cn/blog/package-names。
传递值
不要仅仅为了节省几个字节而将指针作为函数参数传递。如果一个函数在其整个过程中仅通过 *x
引用其参数 x
,那么该参数就不应该是指针。常见的例子包括传递字符串指针(*string
)或接口值指针(*io.Reader
)。在这两种情况下,值本身的大小是固定的,可以直接传递。此建议不适用于大型结构体,甚至可能增长的小型结构体。
接收者名称
方法接收者的名称应反映其身份;通常,其类型的单字母或双字母缩写就足够了(例如,“Client”的“c”或“cl”)。不要使用像“me”、“this”或“self”这样的通用名称,这些标识符通常是面向对象语言中的,它们赋予方法特殊的含义。在 Go 中,方法的接收者只是另一个参数,因此应该相应地命名。该名称不必像方法参数那样具有描述性,因为它的作用是显而易见的,并且没有文档目的。它可以非常简短,因为它会出现在该类型的所有方法的几乎每一行上;熟悉允许简洁。也要保持一致:如果您在一个方法中称接收者为“c”,则不要在另一个方法中称其为“cl”。
接收者类型
选择使用指针接收者还是值接收者来命名方法可能很困难,尤其是对 Go 新手来说。如有疑问,请使用指针,但有时值接收者也是有意义的,通常是为了效率,例如对于小型不变的结构体或基本类型的值。一些有用的指导方针:
- 如果接收者是 map、func 或 chan,请不要使用它们的指针。如果接收者是 slice 并且方法不重新切片或重新分配 slice,请不要使用它的指针。
- 如果方法需要修改接收者,接收者必须是指针。
- 如果接收者是包含 sync.Mutex 或类似同步字段的结构体,则接收者必须是指针以避免复制。
- 如果接收者是大型结构体或数组,指针接收者效率更高。大到什么程度?假设它相当于将所有元素作为参数传递给方法。如果这感觉太大了,那么它对于接收者也太大了。
- 函数或方法,无论是并发地还是从该方法调用时,能否修改接收者?调用方法时,值类型会创建接收者的副本,因此外部更新不会应用于此接收者。如果更改必须在原始接收者中可见,则接收者必须是指针。
- 如果接收者是结构体、数组或 slice,并且其任何元素是指向可能正在变异的内容的指针,则偏向于指针接收者,因为它会向读者更清楚地表明意图。
- 如果接收者是小型数组或结构体,它是自然的值类型(例如,类似 time.Time 类型的东西),没有可变字段也没有指针,或者只是一个简单的基本类型,如 int 或 string,那么值接收者是有意义的。值接收者可以减少可能产生的垃圾量;如果将值传递给值方法,则可以使用栈上副本而不是在堆上分配。(编译器会尝试智能地避免这种分配,但它并不总是能成功。)不要仅为此原因选择值接收者类型,而未先进行性能分析。
- 不要混合接收者类型。为所有可用方法选择指针或结构体类型。
- 最后,如有疑问,请使用指针接收者。
同步函数
优先选择同步函数——直接返回结果或在返回前完成所有回调或 channel 操作的函数——而不是异步函数。
同步函数将 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,“期望 0,得到 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 的一部分。