Gofmt
在你的代码上运行gofmt来自动修复大多数刻板的样式问题。绝大部分的Go代码都会使用gofmt进行格式化代码。本文余下的部分会讲一下写非刻板的样式问题。另一种方法是使用goimports,gofmt的超集,额外添加(和删除)必要的导入行。
注释语句
参考 https://golang.org/doc/effective_go.html#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) { ...
注意除了句号以外,也可以使用(!,?)来结束注释。除此之外,还有很多工具可以用来标识types和method(例如 easyjson: json和golint的MATCH)。这样的规则很难被格式化。
Contexts
context.Context类型的值可以携带一些可以跨API和进程边界的安全凭证,跟踪信息,截止时间,取消信号。Go程序可以明确地传送上下文到函数调用的整个调用链,从进来的RPC和HTTP请求到出去的响应体。
大多数的函数通过第一个参数来接收Context对象:
func F(ctx context.Context, /* other arguments */) {}
一个不需要与特定请求绑定的函数可能会使用context.Background(),即使你认为你不需要上下文也要传递Context参数,要不然就会报错,默认的情况下,我们都会使用context.Background()来直接获得Context对象,除非你有充分的理由说明这么做是错的。
不要添加Context成员到结构体中;而是将ctx参数添加到需要传递该类型的每种方法上。唯一的例外就是调用的方法要与标准库或第三方库中的接口相匹配。
不要创建自定义的Context类型或使用Context以外的接口。
如果你有应用数据需要传递,请将其置于接收器的参数中,使用全局变量,或者如果它真的应该存入Context中,则才将其存入。
Context是不可变的,所以我们可以传递相同的ctx参数到多个方法调用,来共享截止时间,取消信号,凭证,以及追踪信息等等。
Copying
为了避免意外的情形,当从一个包中复制结构体的时候要小心一些。例如,bytes.Buffer类型中包含一个[]byte切片,并且对于短字符串的优化,切片可能会引用一个小的字节数组。如果你复制了Buffer,副本中的切片可能会替换原始的数组,从而导致后续的方法调用会产生奇怪的效果。
一般来说,请不要复制值类型T,如果它的方法中有关联到指针类型*T。
Declaring Empty Slices
当声明空的切片时,优先选择
var t []string
其次是
t := []string{}
前者声明了一个nil的切片,然后后者则声明了一个non-nil的切片,长度为0.他们在功能上其实是等价的—他们的len和cap都是0—但是 nil slice则是更好的方式。
注意这里依然会有一些特别的情况non-nil,0长度的切片会更好一些,例如当我们序列化JSON对象的时候(一个nil的切片会序列化成null,然而[]string{}会序列化成JSON数组[]).
当设计接口的时候,一定要把nil切片和non-nil切片,0长度的切片区分开来,因为他们可能会导致一些细微的程序错误。
关于Go中关于nil的更多讨论请查看Francesc Campoy的文章 Understanding Nil.
Crypto Rand
不要使用math/rand包来生成密钥,即使是一次性的密钥,没有种子的话,密钥生成器是完全可以预测的。若利用time.Nanoseconds()作为种子,则只有很小的时间复杂度。我们可以运用crypto/rand的Reader,作为替代,如果你想输出16位编码的密钥或者是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)
}
Doc Comments
所有上层的,被导出的名字都应该有文档注释,那些没有导出的比较重要类型与方法也应该有相应的注释。关于文档注释的更多信息请查看 https://golang.org/doc/effective_go.html#commentary。
Don’t Panic
请看一下 https://golang.org/doc/effective_go.html#errors. 不要运用panic去做正常的错误处理,运用error类型和多个错误返回来进行错误处理。
Error Strings
错误日志字符串不要以大写字母开头(除非是以合适的名词或缩略词开头的),也不要以标点符号结尾,因为它们会接着其他的上下文。就是这样,使用 fmt.Errorf(“something bad”) 而不要使用 fmt.Errorf(“Something bad”),就像log.Printf(“Reading %s: %v”, filename, err)这种,不应该还有大写字母。这种根本就不适合拿来记录日志,它隐式地面向行,并且不在其他消息内部进行组合。
Examples
添加新软件包时,应包含相应用法的示例:可运行的示例或演示完整调用序列的简单测试。
了解更多可以参考 testable Example() functions.
Goroutine Lifetimes
当你启动Goroutine的时候,一定要搞清楚这个协程什么时候退出以及是否可以退出。Goroutine可以阻塞在发送或接收channel上,从而造成内存泄露:垃圾回收器将不会终止一个goroutine,即使它所阻塞的通道变得不可读写。如果goroutine不泄漏的话,当不再需要此协程的时候仍让它持续运行,这可能会导致其他微妙或难以诊断的问题。向已关闭的channel上发送数据会导致异常。修改仍在使用的输入数据可能会导致数据竞争。 让goroutine持续运行空转任意长的时间可能会导致不可预测的内存使用。尽量保持并发代码足够的简单,来使得goroutine的生命周期显而易见。 如果这样不可行的话,就记录下goroutines退出的时间和原因。
Handle Errors
请看https://golang.org/doc/effective_go.html#errors。不要通过使用 _ 变量符来抛弃一个函数返回的错误,要接收它并检查它是否运行成功。尽量的去处理错误,返回它,或者,确实是一些特殊的情况,我们在对其进行panic。
Imports
尽量避免将导入的包进行重命名;好的包的命名都会不需要重命名的。若一旦发生命名冲突,优先将自己本地的或者特定项目的导入包进行改名。
将导入的包进行分组,并用空格间隔他们。标准库的包总是会在第一组中。
package main
import (
"fmt"
"hash/adler32"
"os"
"appengine/foo"
"appengine/user"
"github.com/foo/bar"
"rsc.io/goversion/version"
)
goimports 将会为你完成这些事情.
Import Dot
这个“import .” 形式在测试中式很有用的,由于环形依赖是不可以在测试的包中存在的:
package foo_test
import (
"bar/testutil" // also imports "foo"
. "foo"
)
在这个例子当中,这个测试文件是不能在foo包中的,因为它引用了bar/testutil包,这个包则引用了foo包。所以我们运用这个“import .”形式来让这个测试文件假装是foo包的一部分,虽然它不是。除此之外,请不要在你的程序里面使用“import .”。因为它会让你的程序很难理解,比如这样会无法很清楚判断你的程序是否有一个叫做Quux的上层函数的定义,还是说这个Quux存在于你所导入的包中。
In-Band Errors
在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对于多个返回的值提供了很好的解决方案。通过调用者来检查错误值,一个函数需要返回一个额外的值来表示返回的其他值是否正确。这个返回的值可以是一个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,“”,和-1这样的函数返回值是正确的。
像某些标准库函数(如包“strings”中的函数)会返回内带的错误返回值。这大大简化了字符串操作的代码,但代价是程序员需要做出更多的付出。一般来说,Go代码都应该返回错误的附加值。
Indent Error Flow
尽量保持常规的代码路径为最小缩进,并首先进行错误的处理。这样提高了代码的可读性,并可以快速扫描常规的代码:
if err != nil {
// error handling
} else {
// normal code
}
可以替代的写法是:
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
如果这个if语句有一个初始化的语句,例如:
x, err := f()
if err != nil {
// error handling
return
}
// use x
然后,这个需要移动短变量声明到if语句的同一行:
if x, err := f(); err != nil {
// error handling
return
} else {
// use x
}
Initialisms
初始字母或首字母缩写词的名称中的单词(例如“URL”或“NATO”)具有一致的情况。 例如,“URL”应该显示为“URL”或“url”(如“urlPony”或“URLPony”),不会显示为“Url”。 举个例子:ServeHTTP不是ServeHttp。 对于具有多个初始化“单词”的标识符,请使用例如“xmlHTTPRequest”或“XMLHTTPRequest”。
这个规则也适用于“ID”,当它是“标识符”的简称时,所以请写“appID”而不是“appId”。
协议缓冲区编译器生成的代码免于此规则。 人工编写的代码比机器编写的代码保持更高的标准。
Interfaces
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{ … } }
Line Length
在Go代码中并没有严格的代码长度限制,但是却还是有必要去避免不舒服的长代码的。同样,不要通过添加换行符来使代码行变短,当很长的代码是可读的,例如那些重复的代码。
大多数情况下,当人们用不自然的方式来包装行(在函数调用或函数声明的中,或多或少地的会有一些例外)时,如果包装合理的参数数量或简短的变量名称,则包装将是不必要的。 对于长的代码行来说,摆脱长名称则会有更大的帮助。
换句话说,由于你写的东西的语义(作为一般规则)而不是因为行的长度而断行。 如果您发现这会产生过长的行,请更改名称或语义,这样可能会是一个更好结果。
实际上,这与关于函数应该有多长的建议完全相同。 没有规则规定“永远不会有超过N行的函数”,但肯定会有这样的事情:函数的名字太长,或者函数名字过于短小,解决方法是改变函数的边界,而不是统计到底有多少行代码。
Mixed Caps
参考一下 https://golang.org/doc/effective_go.html#mixed-caps.即使它打破了其他语言的一些规定。举例说明就是一个未导出的常量是maxLength,而不是MaxLength
或MAX_LENGTH
。
同样可以参考Initialisms。
Named Result Parameters
考虑它在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)
如果函数是只有少数的几行,则裸返回值是可以的。一旦它是一个中等大小的函数,那就请明确你的返回值。推论:仅仅因为它能让你使用裸返回才去运用命名结果参数是不值得的。文档的清晰度总是比在函数中保存一两行更重要。
最后,在某些情况下,你需要使用命名结果参数而在defer函数中来更改结果参数。这总是好的。
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
对于“package main”命令,在二进制命名后其他样式的注释也是可以的(如果在第一行,也要记住首字母大写),举个例子,对于“package main”来说,在seedgen目录中,你应该这样写:
// 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。 取而代之的是,命名文件类型,那些客户端将写入chubby.File。 避免像util,common,misc,api,类型和接口这些毫无意义的软件包名称。 有关更多信息,请参阅http://golang.org/doc/effective_go.html#package-names和http://blog.golang.org/package-names。
Pass Values
若仅仅是为了节省几个字节,那么就请不要使用指针作为函数的参数。在一个函数中,若仅仅是将参数x来当做*x来用,那么则无需使用指针。很多的例子,就比如传指针字符串(*string)或(*io.Reader),这些都是固定大小的,那么就没必要使用指针。但是这个建议并不适用于大的结构体或者小的可变的结构体。
Receiver Names
方法接收者的名字应该能够反映其身份;通常一个单词的一个或两个字母的缩写就足够了(例如“Client”,使用“c”或“cl”)。不要去使用通用的名称,比如“me”,“this”或“self”,这些名称都是面向对象语言的典型关键字,与函数相比,他更强调方法的调用。这个名字不需要向方法论证那样具有描述性,因为它的作用是显而易见的,并且没有任何文档的目的。 它可能很短,因为它几乎出现在每种类型的每种方法的每一行上; 熟悉并且简洁。 也要保持一致:如果您用一种方法调用接收器“c”,则不要在另一个方法中将其称为“cl”。
Receiver Type
对于Gopher来讲,什么时候使用值类型,什么时候使用指针类型作为方法的接收体类型,是一个比较纠结的问题。如果你左右不定的话,那么就请你使用指针类型,但是有的时候使用值类型接收体是会有一定意义的,如果接收体是小的且不变的结构体或者基本类型,这个时候使用值类型作为接收体便可以提高代码的运行效率,下面是一些比较有用的指导:
- 如果接收体是map,func或者chan,那就不要使用指针类型(因为本来它们就是指针类型)。如果接收体是一个slice,并且方法不会调用reslice操作,也不会重新给slice分配内存的话,那就不要使用指针类型。
- 如果方法需要改变接收体,那么这个接收体的类型必须是指针类型。
- 如果接收体中包含sync.Mutex或类似的同步字段结构时,则接收体必须是指针类型,以避免接收体复制后导致同步失效。
- 如果接收体是一个大的结构体或者数组,那么使用指针类型的接收体就会更有效率。多大算大呢?假设把接收体的所有元素作为参数传给方法,如果你觉得参数有点多,那么它就是大的。
- 函数在并发调用接收体的方法时,可以在方法中改变此接收体吗?当并发调用值类型接收体的方法时,这时每一次调用都会复制一份新接收体,所以其他方法对于接收体的修改,并不会对原接收体产生作用。若想让所有的修改在原接收体上起作用,那么接收体的类型必须是指针类型。
- 如果接收体是一个结构体,数组或者切片,并且它们当中有的元素就是指针,它们指向了值可能发生变化的元素,那么就请使用指针类型的接收体,因为它将使读代码的人对于接收体的意图更加的清晰。
- 如果接收体是一个小的数组或者天生就是值类型的结构体(比如 time.Time 类型),而且没有可修改的字段和指针,又或者接收体是一个简单的基本类型,例如int或string,那么就请使用值类型的接收体。这样的话,采用值类型的接收体可以减少生成垃圾的数量; 如果将值传递给值类型接收体的方法,则使用的是堆栈拷贝而不是堆拷贝(编译器尝试智能地避免这种堆分配,但它并不总是成功的)。因此在没有进行性能分析之前,请不要随意的使用值类型的接收体。
- 最后,当你在为了选取哪种类型的接收体而犹豫时,安全起见,请选择指针类型的接收体。
Synchronous Functions
首选同步函数 - 直接返回结果或返回之前完成任何回调或通道操作的函数 - 相比异步处理函数。
同步函数可以将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,“期望0,得到x”等等。 Go没有。
如果这看起来像是要打很多的字,你可能要写一个表驱动的测试。
在使用不同输入的测试助手时,另一种常见的消除测试失败问题的方法是用不同的TestFoo函数包装每个调用者,以便测试失败时使用该名称:
func TestSingleValue(t *testing.T) { testHelper(t, []int{80}) }
func TestNoValues(t *testing.T) { testHelper(t, []int{}) }
在以后的情况下,你的测试都会失败的,并向将来调试代码的人发送有用的信息。
Variable Names
Go语言中的变量命名应该是短命名好过长命名,特别是对于有空间限制的局部变量。最好是命名成c而不是lineCount,命名成i而不是sliceIndex。基本规则:若变量的使用离它的声明越远,那就越应该使变量名更具有描述性。对于方法接收体,一个或两个字母就足够了。常见变量如循环索引和读取值可以是单个字母(i,r)。越是那些特殊的东西和全局变量就越需要更具有描述性的名称。