Go Wiki:Go 代码审查评论

此页面收集了 Go 代码审查期间提出的常见评论,以便通过简写来参考单一的详细解释。这是一份常见的样式问题的清单,而不是全面的样式指南。

您可以将此视为 有效的 Go 的补充。

有关测试的附加评论可在 Go 测试评论 中找到

Google 已发布更长的 Go 样式指南

讨论更改,然后再编辑此页面,即使是次要更改。许多人都有意见,这不是进行编辑争论的地方。

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) { ...

等等。

上下文

context.Context 类型的值跨 API 和进程边界传递安全凭据、跟踪信息、截止时间和取消信号。Go 程序从传入的 RPC 和 HTTP 请求到传出的请求,显式地沿着整个函数调用链传递 Context。

大多数使用 Context 的函数都应将其作为其第一个参数接受

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

一个永远不会针对特定请求的函数可以使用 context.Background(),但即使你认为不需要,也要传递 Context。默认情况下,传递 Context;只有在你有充分理由认为替代方案是错误的情况下,才直接使用 context.Background()。

不要向结构类型添加 Context 成员;相反,向该类型上需要传递 Context 的每个方法添加一个 ctx 参数。唯一的例外是其签名必须与标准库或第三方库中的接口匹配的方法。

不要创建自定义 Context 类型或在函数签名中使用 Context 以外的接口。

如果你有要传递的应用程序数据,请将其放在参数中、接收器中、全局变量中,或者如果它确实属于那里,则放在 Context 值中。

Context 是不可变的,因此将相同的 ctx 传递给共享相同截止时间、取消信号、凭据、父跟踪等的多个调用是可以的。

复制

为避免意外别名,在从其他包中复制结构时要小心。例如,bytes.Buffer 类型包含一个 []byte 切片。如果你复制一个 Buffer,则复制中的切片可能会别名原始数组,导致后续方法调用产生意外效果。

一般来说,如果其方法与指针类型 *T 相关联,则不要复制类型为 T 的值。

Crypto Rand

不要使用包 math/rand 来生成密钥,即使是临时密钥。未设置种子时,生成器完全可预测。使用 time.Nanoseconds() 设置种子时,只有少量熵。相反,使用 crypto/rand 的 Reader,如果你需要文本,则打印为十六进制或 base64

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)
}

声明空切片

在声明空切片时,优先使用

var t []string

而不是

t := []string{}

前者声明一个 nil 切片值,而后者是非 nil 但长度为零。它们在功能上是等效的——它们的 lencap 都是零——但 nil 切片是首选样式。

请注意,在有限的情况下,优先使用非 nil 但长度为零的切片,例如在编码 JSON 对象时(nil 切片编码为 null,而 []string{} 编码为 JSON 数组 [])。

在设计接口时,避免区分 nil 切片和非 nil、长度为零的切片,因为这会导致微妙的编程错误。

有关 Go 中 nil 的更多讨论,请参阅 Francesc Campoy 的演讲 理解 Nil

文档注释

所有顶级导出名称都应有文档注释,非平凡的未导出类型或函数声明也应有文档注释。有关注释约定的更多信息,请参阅 https://golang.ac.cn/doc/effective_go#commentary

不要惊慌

请参阅 https://golang.ac.cn/doc/effective_go#errors。不要对常规错误处理使用 panic。使用错误和多个返回值。

错误字符串

错误字符串不应大写(除非以专有名词或首字母缩写词开头)或以标点符号结尾,因为它们通常在其他上下文之后打印。也就是说,使用 fmt.Errorf("something bad") 而不是 fmt.Errorf("Something bad"),以便 log.Printf("Reading %s: %v", filename, err) 格式化时不会在消息中间出现虚假的大写字母。这不适用于日志记录,日志记录隐式地以行为导向,并且不会与其他消息组合在一起。

示例

在添加新包时,包括预期用法示例:一个可运行的 Example,或一个演示完整调用序列的简单测试。

阅读更多有关 可测试的 Example() 函数 的信息。

Goroutine 生命周期

生成协程时,要明确它们何时(或是否)退出。

协程可能会因阻塞在通道发送或接收上而泄漏:即使协程所阻塞的通道不可达,垃圾回收器也不会终止协程。

即使协程没有泄漏,在不再需要时让它们处于飞行状态也会导致其他难以诊断的微妙问题。向已关闭的通道发送数据会引发恐慌。在“结果不再需要”后修改仍在使用的输入仍然会导致数据竞争。让协程在飞行状态下停留任意长时间会导致内存使用不可预测。

尝试保持并发代码足够简单,以使协程生命周期显而易见。如果这不可行,请记录协程何时以及为何退出。

处理错误

请参阅 https://golang.ac.cn/doc/effective_go#errors。不要使用 _ 变量丢弃错误。如果函数返回错误,请检查它以确保函数成功。处理错误、返回错误,或在真正特殊的情况下引发恐慌。

导入

避免重命名导入,除非是为了避免名称冲突;好的包名称不应该需要重命名。如果发生冲突,最好重命名最本地或特定于项目的导入。

导入按组组织,组之间用空行分隔。标准库包始终位于第一组。

package main

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

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

goimports 会为您执行此操作。

导入空白

仅因其副作用而导入的包(使用语法 import _ "pkg")只应导入到程序的主包中,或导入到需要它们的测试中。

导入点

导入 . 形式可用于由于循环依赖而无法成为被测包一部分的测试中

package foo_test

import (
    "bar/testutil" // also imports "foo"
    . "foo"
)

在这种情况下,测试文件不能在包 foo 中,因为它使用了 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 对多返回值的支持提供了一个更好的解决方案。函数无需要求客户端检查带内错误值,而应返回一个附加值来指示其其他返回值是否有效。此返回值可以是错误,也可以是在不需要解释时的布尔值。它应该是最终返回值。

// 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”,当它表示“标识符”时(在它不是“id”(如“ego”、“superego”)的情况下几乎所有情况都是如此),因此应编写“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://golang.ac.cn/doc/effective_go#mixed-caps。即使它打破了其他语言中的约定,这也适用。例如,一个未导出的常量是 maxLength,而不是 MaxLengthMAX_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)

如果函数只有几行,那么裸返回是可以的。一旦它成为一个中等大小的函数,请明确你的返回值。推论:仅仅因为这使你能够使用裸返回,就不值得命名结果参数。文档的清晰度始终比在你的函数中节省一两行更重要。

最后,在某些情况下,你需要命名一个结果参数,以便在延迟闭包中更改它。这总是可以的。

裸返回

没有参数的 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://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-nameshttps://golang.ac.cn/blog/package-names

传递值

不要仅仅为了节省几个字节而将指针作为函数参数传递。如果一个函数在整个过程中只将它的参数x引用为*x,那么该参数不应该是一个指针。常见的实例包括传递一个指向字符串的指针(*string)或一个指向接口值的指针(*io.Reader)。在这两种情况下,值本身都是固定大小的,可以直接传递。此建议不适用于大型结构体,甚至可能增长的小型结构体。

接收器名称

方法接收者的名称应该是其标识的反映;通常,其类型的缩写(例如“c”或“cl”表示“Client”)就足够了。不要使用诸如“me”、“this”或“self”之类的通用名称,这些标识符通常用于面向对象语言,赋予方法特殊含义。在 Go 中,方法的接收者只是另一个参数,因此应该相应地命名。名称不必像方法参数那样具有描述性,因为它的作用很明显,并且不具有记录目的。它可以非常短,因为它将出现在类型的每个方法的几乎每一行上;熟悉允许简洁。也要保持一致:如果你在一种方法中将接收者称为“c”,请不要在另一种方法中将其称为“cl”。

接收器类型

选择在方法上使用值接收者还是指针接收者可能很困难,尤其是对于新的 Go 程序员。如有疑问,请使用指针,但在某些情况下,值接收者是有意义的,通常出于效率原因,例如对于小型不变结构体或基本类型的值。一些有用的准则

同步函数

优先使用同步函数 - 直接返回其结果或在返回之前完成任何回调或通道操作的函数 - 而不是异步函数。

同步函数将 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

基本规则:名称使用的离其声明越远,名称就必须越具描述性。对于方法接收者,一个或两个字母就足够了。常见的变量(例如循环索引和读取器)可以是单个字母(ir)。更不常见的对象和全局变量需要更具描述性的名称。

另请参阅 Google Go 风格指南 中的更详细讨论。


此内容是 Go Wiki 的一部分。