主要参考了 Google 对外发布的编程规范:Go Code Review Comments 和内部的一些使用习惯。
两个小工具
- gofmt:帮助你自动格式化代码
- goimports:帮助你自动添加/删除包的引用
代码注释的语句
更多参见:https://golang.org/doc/effective_go.html#commentary
尽管有时候会显得比较多余,但是代码注释最好是完整的语句,大写开头,句号结尾。
这样做的好处是,在自动生成 godoc 文档时,会产生更好的格式。
// A 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
如果需要 Contexts 的话,将它作为方法的第一个参数传入:
func F(ctx context.Context, /* other arguments */) {}
- 不要将 Context 作为一个结构体成员变量
- 不要自定义 Context 类型
- Context 是一个 immutable 不可变对象
复制 Copying
例如,bytes.Buffer
类型包含一个 []byte
切片。当你去复制一个 Buffer
对象时,实际最后都是指向同一个 []byte
。
定义空的切片 Declaring Empty Slices
推崇的方式 var t []string
:定义了个 nil
不推崇的方式 t := []string{}
:不是 nil
,但是长度为 0
不过对上面这两种方式定义的变量,调用 len()
和 cap()
的结果都是 0
。
加密随机数 Crypto Rand
Do not use package math/rand to generate keys, even throwaway ones. Unseeded, the generator is completely predictable. Seeded with time.Nanoseconds(), there are just a few bits of entropy. Instead, use crypto/rand's Reader, and if you need text, print to hexadecimal or base64:
不要使用 math/rand
来产生加密的 keys,因为 math/rand
的随机数算法是可预测的。
推荐使用 crypto/rand
,也可以将结果转换成 hexadecimal
或者 base64
。
示例:
import (
"fmt"
"crypto/rand"
)
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)
}
func main() {
fmt.Println(Key()) // 40ca8b6ff7e65501b097cc0e9aebdc2e
fmt.Println(Key()) // 5faaad1a34483977420349e980954b6f
}
文档注释 Doc Comments
Exported 导出的方法/变量需要有注释。
不要使用 Panic Don't Panic
不要使用 Panic 来进行异常的处理,例如:
import (
"os"
)
func main() {
_, err := os.Create("/tmp/file")
if err != nil {
panic(err) // 不推荐
}
}
如果你真的想要程序马上退出的话,可以使用 log.Exit
。否则,可以使用 log.Fatal
来描述一般的错误。
异常语句 Error Strings
异常语句不要以大写开头,结尾也不要有标点符号。
推荐:fmt.Errorf("something bad")
不推荐:fmt.Errorf("Something bad")
什么原因呢?因为通常异常都会被捕获,最后作为日志的一部分打印出来,所以就最好不要有大写字母和标点符号。
因此对于日志语句而言,可以大写开头,也可以标点符号结尾,例如 log.Printf("Reading %s: %v", filename, err)
。
示例 Examples
在添加新的 package 时,可以添加一些示例,会自动添加到 godoc 文档中,例如:
Goroutine Lifetimes 生命周期
使用 goroutines 时候,确保他们及时退出。
处理异常 Handle Errors
不要使用 _
来忽略异常,例如 res, _ := func()
。
要检查并处理异常,例如:
res, err := func()
if err != nil {
// Handle the error, return it, or, in truly exceptional situations, call log.Fatal or, if necessary, panic.
}
包的导入 Imports
尽量不用重命名 package,包的引用最好进行分组,使用空白行分隔,例如:
import (
"fmt"
"hash/adler32"
"os"
"appengine/foo"
"appengine/user"
"github.com/foo/bar"
"rsc.io/goversion/version"
)
ImportBlank
Go 语言要求导入的包必须在后续中使用,否则会报错。
如果想要避免这个错误,可以在包的前面加上下划线 _
,例如 _ "net/http"
。
问题来了,如果一个包不被使用,那为什么要导入呢?
因为导入匿名包仅仅表示无法再访问其内的属性。但导入这个匿名包的时候,会进行一些初始化操作,例如 init()
,如果这个初始化操作会影响当前包,那么这个匿名导入就是有意义的。
例如:
import (
"database/sql"
_ "github.com/lib/pq" // 我们需要的是这个包里面的 init() 方法
)
Import Dot
如果不想在访问包属性的时候加上包名,则导入的时候,可以为其设置特殊的别名:点 .
,例如:
import (
. "fmt"
)
func main() {
Println() // 无需包名,直接访问Println
}
In-Band Errors
不要返回 -1
或者空指针来代表出现了异常。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)
这样的话,我们就可以通过如下的方式来调用该方法:
value, ok := Lookup(key)
if !ok {
return fmt.Errorf("no value for %q", key)
}
return Parse(value)
Indent Error Flow
保持缩进尽量的少。
例如下面一段代码:
if err != nil {
// error handling
} else {
// normal code
}
我们可以修改为如下的方式,从而减少 normal code 的缩进:
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
Initialisms
对于缩略词,保持统一的大小写。
推荐 URL
,不推荐 Url
。
推荐 ServeHTTP
,不推荐 ServeHttp
。
推荐 appID
,不推荐 appId
。
接口 Interfaces
Do not define interfaces on the implementor side of an API "for mocking"; instead, design the API so that it can be tested using the public API of the real implementation.
Do not define interfaces before they are used: without a realistic example of usage, it is too difficult to see whether an interface is even necessary, let alone what methods it ought to contain.
Line Length
Go 语言对每一行的长度没有明确的限制。
Mixed Caps
对于多个单词组成的名字,使用 MixedCaps,而不是使用下划线分割。
例如,推荐 maxLength
,不推荐 MaxLength
和 MAX_LENGTH
。
Named Result Parameters VS Naked Returns
在定义函数的返回值的时候,需不需要命名呢?
对比下面的两个函数定义:
func (n *Node) Parent1() *Node
func (n *Node) Parent1() (node *Node)
前者更简单明了。
但是如果某个函数有很多个返回值,并且每个返回值的意义不容易从上下文中推断的话,建议命名。
对比下面的两个函数定义:
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)
后者表达的更清楚,并且能体现到 godoc 中。
包的注释 Package Comments
添加在包名的上方,大写开头,句号结尾。例如:
// 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 Names
- 如果已经定义好了包名,例如
chubby
,那么这个包里面定义的其他东西,就不用再加上包名了。- 例如不需要定义一个结构体叫做
ChubbyFile
,这样别人在使用的时候,就会出现chubby.ChubbyFile
,略显重复。 - 我们直接将该结构体命名为
File
,这样别人在使用的时候,就是chubby.File
,简单明了。
- 例如不需要定义一个结构体叫做
- 避免使用没有意义或者意义过于宽泛的名字,例如
util
,common
,misc
,api
,types
,interfaces
等等。
使用值来传递函数的参数 Pass Values
不要为了节省空间,而使用指针来传递函数的参数。
几个特例:
- 对于大的结构体,或者会增大的结构体,用指针来传递函数的参数。
- 对于 protocol buffer 类型,用指针来传递函数的参数。
用什么变量名来接收函数返回的结果 Receiver Names
Go 语言推崇使用一两个字母的缩写。
例如,用 c
或者 cl
替代 client
。
用什么类型来接收函数返回的结果 Receiver Type
到底是用值还是指针来接收函数返回的结果?
Go 语言推崇使用指针。有时候也可以使用值来接收,特别是对于一些不会改变的结构体或者原始类型。
一些基本原则如下:
- 如果函数返回的是 map,函数或者是 slice,不要使用指针。
- 如果函数需要修改这个返回值,那么需要使用指针。
- 如果函数返回的是一个结构体,并且包含了
sync.Mutex
或者类似的同步字段,那么需要使用指针,来避免内容的复制。 - 如果函数返回的是一个大的结构体或者数组,那么使用指针更高效。
- 如果使用值来接收,实际上是产出了内容的拷贝,对内容的修改不会影响函数内部。如果想要结果对函数内部可见,需要使用指针。
- 如果函数返回的是一个结构体或者数组或者 slice 切片,并且里面有指针类型的成员,那么推荐使用指针来接收。
- 如果函数返回的是小的一个结构体或者数组,并且里面的都是值类型的成员,例如
time.Time
或者int
,那么推荐使用值来接收,这样更高效。原因:使用值接收的话,是直接在栈空间上进行复制,而使用指针的话,需要分配堆空间。
返回同步结果的函数 Synchronous Functions
Go 语言推崇直接返回同步结果的函数。
打印有用的测试错误信息 Useful Test Failures
当单元测试出现错误时,需要打印有用的信息,包括输入是什么,实际结果是什么,期望结果是什么。例如:
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
}
注意:Go 语言推崇将实际结果放在前面,期望结果放在后面。
变量名 Variable Names
Go 语言推崇短的变量名,特别是局部变量。
例如,用 c
替代 lineCount
,用 i
替代 sliceIndex
。
一个基本原则是,如果一个变量在将来会被更多的用到,那么这个变量名可以取得更有意义,否则就应该用短的名字。
断言 Assert
Go 语言不推荐在测试中使用断言,例如:assert.isNotNil(t, "obj", obj)
,它会使得测试提前结束,或者丢失掉有用的信息。
Go 语言推荐如下的方式:
if obj == nil {
t.Errorf("AddPost() = %+v", obj)
}
如果要比较两个对象,可以使用 cmp.Equal
。
如果要比较两个 Protocol buffers,可以使用 proto.Equal
或者 messagediff.Compare
。
Getters
对于成员变量,Go 语言不推荐在 Get 方法前添加 Get
前缀,例如:owner := obj.GetOwner()
,推荐直接使用 owner := obj.Owner()
。
SwitchBreak
不同于 Java 等语言,Go 语言在 switch
中会自动 break
,因此不需要手动添加 break
。
switch x {
case "A", "B":
buf.WriteString(x)
case "C":
// handled outside of the switch statement
default:
return fmt.Errorf("unknown value: %q", x)
}
测试包 TestPackage
单元测试的文件可以放在被测试文件同一个包中,命名为 foo_test.go
。
Use %q
%q
会将字符串包裹在双引号中。
例如:
fmt.Printf("value %q", "Hello") // value "Hello"
fmt.Printf("value '%s'", "Hello") // value 'Hello'