Go Code Review Comments 译文

持续更新中…
原文最新连接
https://github.com/golang/go/wiki/CodeReviewComments/5a40ba36d388ff1b8b2dd4c1c3fe820b8313152f
Github译文连接
https://github.com/wddpct/articles/issues/8html

对于刚开始学习和使用 Go 的新手来讲,有这么几个资源值得关注。node

  1. A Tour of Go
  2. How to Write Go Code
  3. Frequently Asked Questions (FAQ)
  4. The Go Blog Index
  5. The Go Wiki
  6. Effective Go
  7. Go Code Review Comments
  8. ……

Effective Go 和 Go Code Review Comments 中介绍了不少有助于编写优雅,高效的 Go 代码的指导性原则,前者能够认为是官方权威指南,然后者则能够算是对前者的补充,原文托管在 Github 上,每月都会作部分 fix,而网上的中文版大多都是历史版本,没法及时更新,因此想由本身在业余时作些额外的工做,水平通常,能力有限,少部分难以翻译的词句将附上原文或给出意译内容。如下是翻译正文。git


Go Code Review Comments

当前页面收集了在 Go 代码审核期间的常见意见,以便一个详细说明能被快速参考。这是一个常见错误的清单,而非综合性的风格指南。程序员

你也能够将它做为是 Effective Go 的补充。github

请在编辑这个页面前先讨论这个变动,就算是一个很小的变动。毕竟许多人都有本身的想法,而这里并非战场。golang

Gofmt

在 Go 代码上运行 gofmt 以自动修复大多数的机械性风格问题。几乎全部不正规的 Go 代码都在使用gofmt。本文档的剩余部分涉及非机械性风格问题。编程

另外一种方法是使用 goimports,这是gofmt的超集,gofmt可根据须要额外添加(和删除)导入行。json

Comment Sentences

参见 https://golang.org/doc/effective_go.html#commentary。注释文档声明应该是完整的句子,即便这看起来有些多余。这种方式使注释在提取到 godoc 文档时格式良好。注释应以所描述事物的名称开头,并以句点结束:swift

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

请注意除了句点以外还有其余符号能够做为句子的有效结尾(但至少也应该是!,?)。除此以外,还有许多工具使用注释来标记类型和方法(如 easyjson:json 和 golint 的 MATCH)。这使得这条规则难以形式化。api

Contexts

A function that is never request-specific may use context.Background(), but err on the side of passing a Context even if you think you don't need to. The default case is to pass a Context; only use context.Background() directly if you have a good reason why the alternative is a mistake.

context.Context 类型的值包含跨 API 和进程边界的安全凭证,跟踪信息,截止时间和取消信号。好比传入 RPC 请求和 HTTP 请求一直到传出相关请求,Go 程序在整个过程的函数调用链中显式地传递 Context。

大多数使用 Context 的函数都应该接受 Context 做为函数的第一个参数:

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

从不特定于请求(request-specific)的函数可使用 context.Background() 获取 Context,并将 err 与 Context 同时传递,即便你认为不须要。默认状况下只传递 Context ;只在你有充分的理由认为这是错误的,才能直接使用context.Background()。

原文: A function that is never request-specific may use context.Background(), but err on the side of passing a Context even if you think you don't need to. The default case is to pass a Context; only use context.Background() directly if you have a good reason why the alternative is a mistake.

不要将 Context 成员添加到某个 struct 类型中;而是将 ctx 参数添加到该类型的方法上。一个例外状况是当前方法签名必须与标准库或第三方库中的接口方法匹配。

不要在函数签名中建立自定义 Context 类型或使用除了 Context 之外的接口。

若是要传递应用程序数据,请将其放在参数,方法接收器,全局变量中,或者若是它确实应该属于 Context,则放在 Context 的 Value 属性中。

全部的 Context 都是不可变的,所以能够将相同的 ctx 传递给多个共享相同截止日期,取消信号,安全凭据,跟踪等的调用。

Copying

为避免意外的别名,从另外一个包复制 struct 时要当心。例如,bytes.Buffer 类型包含一个 []byte 的 slice,而且做为短字符串的优化,slice 能够引用一个短字节数组。若是复制一个 Buffer,副本中的 slice 可能会对原始数组进行别名操做,从而致使后续方法调用产生使人惊讶的效果。

一般,若是 T 类型的方法与其指针类型 *T 相关联,请不要复制 T 类型的值。

Crypto Rand

不要使用包math/rand来生成密钥,即便是一次性密钥。在没有种子(seed)的状况下,生成器是彻底能够被预测的。使用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)
}

Declaring Empty Slices

当声明一个空 slice 时,倾向于用

var t []string

代替

t := []string{}

前者声明了一个 nil slice 值,然后者声明了一个非 nil 可是零长度的 slice。二者在功能上等同,len 和 cap 均为零,而 nil slice 是首选的风格。

请注意,在部分场景下,首选非零但零长度的切片,例如编码 JSON 对象时(前者编码为 null,然后者则能够正确编码为 JSON array[])。

在设计 interface 时,避免区分 nil slice 和 非 nil,零长度的 slice,由于这会致使细微的编程错误。

有关 Go 中对于 nil 的更多讨论,请参阅 Francesc Campoy 的演讲 Understanding Nil

Doc Comments

全部的顶级导出的名称都应该有 doc 注释,重要的未导出类型或函数声明也应如此。有关注释约束的更多信息,请参阅 https://golang.org/doc/effective_go.html#commentary

Don't Panic

请参阅 https://golang.org/doc/effective_go.html#errors。不要将 panic 用于正常的错误处理。使用 error 和多返回值。

Error Strings

Error strings should not be capitalized (unless beginning with proper nouns or acronyms) or end with punctuation, since they are usually printed following other context. That is, use fmt.Errorf("something bad") not fmt.Errorf("Something bad"), so that log.Printf("Reading %s: %v", filename, err) formats without a spurious capital letter mid-message. This does not apply to logging, which is implicitly line-oriented and not combined inside other messages.

错误信息字符串不该大写(除非以专有名词或首字母缩略词开头)或以标点符号结尾,由于它们一般是在其余上下文后打印的。即便用fmt.Errorf("something bad")而不要使用fmt.Errorf("Something bad"),所以log.Printf("Reading %s: %v", filename, err)的格式中将不会出现额外的大写字母。不然这将不适用于日志记录,由于它是隐式的面向行,而不是在其余消息中组合。

Examples

When adding a new package, include examples of intended usage: a runnable Example, or a simple test demonstrating a complete call sequence.

Read more about testable Example() functions.

添加新包时,请包含预期用法的示例:可运行的示例,或是演示完整调用链的简单测试。

阅读有关 testable Example() functions 的更多信息。

Goroutine Lifetimes

当你生成 goroutines 时,要清楚它们什么时候或是否会退出。

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

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

请尽可能让并发代码足够简单,从而更容易地确认 goroutine 的生命周期。若是这不可行,请记录 goroutines 退出的时间和缘由。

Handle Errors

请参阅 https://golang.org/doc/effective_go.html#errors。不要使用 _ 变量丢弃 error。若是函数返回 error,请检查它以确保函数成功。处理 error,返回 error,或者在真正特殊的状况下使用 panic。

Imports

Avoid renaming imports except to avoid a name collision; good package names should not require renaming. In the event of collision, prefer to rename the most local or project-specific import.

避免包重命名导入,防止名称冲突;好的包名称不须要重命名。若是发生命名冲突,则更倾向于重命名最接近本地的包或特定于项目的包。

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

package main

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

    "appengine/foo"
    "appengine/user"

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

goimports 会为你作这件事。

Import Dot

部分包因为循环依赖,不能做为测试包的一部分进行测试时,以.形式导入它们可能颇有用:

package foo_test

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

在这种状况下,测试文件不能位于 foo 包中,由于它使用的 bar/testutil 依赖于 foo 包。因此咱们使用import .形式使得测试文件假装成 foo 包的一部分,即便它不是。除了这种状况,不要在程序中使用 import .。它将使程序更难阅读——由于不清楚如 Quux 这样的名称是不是当前包中或导入包中的顶级标识符。

In-Band Errors

在 C 和类 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 a 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 对多返回值的支持提供了一种更好的解决方案。函数应返回一个附加值以指示其余返回值是否有效,而不是要求客户端检查 in-band 错误值。此附加值多是一个 error,或者在不须要解释时能够是布尔值。它应该是最终的返回值。

// 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” 包中的函数)会返回 in-band 错误值。这大大简化了字符串操做,代价是须要程序员作更多事。一般,Go 代码应返回表示错误的附加值。

Indent Error Flow

Try to keep the normal code path at a minimal indentation, and indent the error handling, dealing with it first. This improves the readability of the code by permitting visually scanning the normal path quickly. For instance, don't write:

尝试将正常的代码路径保持在最小的缩进处,优先处理错误并缩进。经过容许快速可视化扫描正常路径来提升代码的可读性。例如,不要写:

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

名称中的单词是首字母或首字母缩略词(例如 “URL” 或 “NATO” )须要具备相同的大小写规则。例如,“URL” 应显示为 “URL” 或 “url” (如 “urlPony” 或 “URLPony” ),而不是 “Url”。举个例子:ServeHTTP 不是 ServeHttp。对于具备多个初始化 “单词” 的标识符,也应当显示为 “xmlHTTPRequest” 或 “XMLHTTPRequest”。

当 “ID” 是 “identifier” 的缩写时,此规则也适用于 “ID” ,所以请写 “appID” 而不是“appId”。

由协议缓冲区编译器生成的代码不受此规则的约束。人工编写的代码比机器编写的代码要保持更高的标准。

Interfaces

Go 接口一般属于使用 interface 类型值的包,而不是实现这些值的包。实现包应返回具体(一般是指针或结构)类型:这样一来能够将新方法添加到实现中,而无需进行大量重构。

不要在 API 的实现者端定义 “for 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{ … } }

相反,返回一个具体的类型,让消费者模拟生产者实现。

package producer

type Thinger struct{ … }
func (t Thinger) Thing() bool { … }

func NewThinger() Thinger { return Thinger{ … } }

Line Length

Go代码中没有严格的行长度限制,但避免使用形成阅读障碍的长行。相似的,若是长行的可读性更好,不要为了缩短行而添加换行符——例如,行组成是重复的。

大多数状况下,当人们 “不天然地” 自动换行(wrap lines)时(在函数调用或函数声明的中间,或多或少,好比,虽然有一些例外),若是它们有合理数量的参数而且变量名称较短时,自动换行将是没必要要的。长行彷佛与长名称有关,避免名称过长有很大帮助。

换句话说,换行是由于你所写的语义(做为通常规则)而不是由于行的长度。若是您发现这会产生太长的行,那么更更名称或语义,可能也会获得一个好结果。

实际上,这与关于函数应该有多长的建议彻底相同。没有 “永远不会有超过N行的函数” 这样的规则,可是程序中确定会存在行数太多,功能过于微弱的函数,而解决方案是改变这个函数边界的位置,而不是执着在行数上。

Mixed Caps

请参阅 https://golang.org/doc/effective_go.html#mixed-caps。即便 Go 中混合大小写的规则打破了其余语言的惯例,也是适用的。例如,未导出的常量写成 maxLength 而不是MaxLength或MAX_LENGTH。

另见当前页面的 Initialisms 一节。

Named Result Parameters

考虑一下 godoc 中会是什么样子。命名结果参数如:

func (n *Node) Parent1() (node *Node) func (n *Node) Parent2() (node *Node, err error)

将会形成口吃现象(stutter); 最好这样使用:

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)

若是函数行数较少,那么非命名结果参数是能够的。一旦它是一个中等规模的函数,请明确返回值。推论:仅仅由于它使得可以直接使用预命名返回而命名结果参数是不值得的。文档的清晰度总比在函数中的一行两行更重要。

最后,在某些状况下,您须要命名结果参数,以便在延迟闭包中更改它,这也是能够的。

Naked Returns

请参阅当前页面 Named Result Parameters 一节。

Package Comments

与 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

For "package main" comments, other styles of comment are fine after the binary name (and it may be capitalized if it comes first), For example, for a package main in the directory seedgen you could write:

对于 “package main” 注释,在二进制文件名称以后可使用其余样式的注释(若是它们放在前面,则能够大写),例如,对于你能够编写 seedgen 目录中下的 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.org/doc/effective_go.html#commentary。

Package Names

包中名称的全部引用都将使用包名完成,所以您能够从标识符中省略该名称。例如,若是有一个 chubby 包,你不该该定义类型名称为 ChubbyFile ,不然使用者将写为 chubby.ChubbyFile。而是应该命名类型名称为 File,使用时将写为 chubby.File。避免使用无心义的包名称,如 util,common,misc,api,types 和 interfaces。有关更多信息,请参阅http://golang.org/doc/effective_go.html#package-names和 http://blog.golang.org/package-names

Pass Values

不要只是为了节省几个字节就将指针做为函数参数传递。若是一个函数在整个过程当中只引用它的参数x做为x,那么这个参数不该该是一个指针。此常见实例包括将指针传递给 string(string)或是指向接口值(*io.Reader)的指针。在这两种状况下,值自己都是固定大小,能够直接传递。这个建议不适用于大型 struct ,甚至不适用于可能生长的小型 struct。

Receiver Names

方法接收者的名称应该反映其身份;一般,其类型的一个或两个字母缩写就足够了(例如“client”的“c”或“cl”)。不要使用通用名称,例如“me”,“this”或“self”,这是面向对象语言的典型标识符,它们更强调方法而不是函数。名称没必要像方法论证那样具备描述性,由于它的做用是显而易见的,不起任何记录目的。名称能够很是短,由于它几乎出如今每种类型的每一个方法的每一行上;familiarity admits brevity。使用上也要保持一致:若是你在一个方法中叫将接收器命名为“c”,那么在其余方法中不要把它命名为“cl”。

Receiver Type

选择究竟是在方法上使用值接收器仍是使用指针接收器可能会很困难,尤为是对于 Go 新手程序员。若有疑问,请使用指针接收器,但有时候值接收器是有意义的,一般是出于效率的缘由,例如小的不变结构或基本类型的值。如下是一些有用的指导:

  • 若是接收器是 map,func或 chan,则不要使用指向它们的指针。若是接收器是 slice 而且该方法不从新切片或不从新分配切片,则不要使用指向它的指针。
  • 若是该方法须要改变接收器的值,则接收器必须是指针。
  • 若是接收器是包含 sync.Mutex 或相似同步字段的 struct,则接收器必须是避免复制的指针。
  • 若是接收器是大型结构或数组,则指针接收器更有效。多大才算大?假设它至关于将其包含的全部元素做为参数传递给方法。若是感受太大,那么对接收器来讲也太大了。
  • 函数或方法能够改变接收器吗(并发调用或调用某方法时继续调用相关方法或函数)?在调用方法时,值类型会建立接收器的副本,所以外部更新将不会应用于此接收器。若是必须在原始接收器中看到更改效果,则接收器必须是指针。
  • 若是接收器是 struct,数组或 slice,而且其任何元素是指向可能改变的对象的指针,则更倾向于使用指针接收器,由于它将使读者更清楚地意图。
  • 若是接收器是一个小型数组或 struct,那么它天然是一个值类型(例如,相似于time.Time类型),对于没有可变字段,没有指针的类型,或者只是一个简单的基本类型,如 int 或 string,值接收器是合适的。值接收器能够减小能够生成的垃圾量;若是将值做为参数传递给值类型方法,则可使用堆栈上的副本而不须要在堆上进行分配。(编译器试图避免这种分配,但它不能老是成功)所以,在没有进行分析以前,不要选择值接收器类型。
  • 最后,若有疑问,请使用指针接收器。

Synchronous Functions

相比异步函数更倾向于同步函数——直接返回结果的函数,或是在返回以前已完成全部回调或 channel 操做的函数。

同步函数让 goroutine 在调用中本地化,可以更容易地推断其生命周期并避免泄漏和数据竞争。同步函数也更容易测试:调用者能够传递输入并检查输出,而无需轮询或同步。

若是调用者须要更多的并发性,他们能够定义和调用单独的 goroutine 中的函数来轻松实现。可是在调用者端删除没必要要的并发性是很是困难的——有时是不可能的。

Useful Test Failures

失败的测试也应该提供有用的消息,说明错误,展现输入内容,实际内容以及预期结果。编写一堆 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 }

请注意,此处的命令是 实际结果!=预期结果,而且错误消息也使用该命令格式。然而一些测试框架鼓励倒写输出格式,如 预期结果 != 实际结果,“预期结果为 0,实际结果为 x”,等等。可是 Go 没有这样作。

若是这看起来像是打了不少字,你可能想写一个表驱动的测试。

在使用具备不一样输入的测试帮助程序时以消除失败测试歧义的另外一种常见技术是使用不一样的 TestFoo 函数包装每一个调用者,而测试名称也根据对应的输入命名:

func TestSingleValue(t *testing.T) { testHelper(t, []int{80}) }
func TestNoValues(t *testing.T)    { testHelper(t, []int{}) }

In any case, the onus is on you to fail with a helpful message to whoever's debugging your code in the future.

在任何状况下,你都有责任向可能会在未来调试你的代码的开发者提供有用的消息。

Variable Names

Go 中的变量名称应该短而不是长。对于范围域中的局部变量尤为如此。例如为 line count 定义 c 变量,为 slice index 定义 i 变量。

基本规则:范围域中,越晚使用的变量,名称必须越具备描述性。对于方法接收器,一个或两个字母就足够了。诸如循环索引和读取器(Reader)之类的公共变量能够是单个字母(i,r)。更多不寻常的事物和全局变量则须要更具描述性的名称。

相关文章
相关标签/搜索