编码风格
相似的声明放在一组
Go支持将相似的声明放在一个组内
Bad
import "a"
import "b"
Good
import (
"a"
"b"
)
这同样适用于常量、变量和类型声明
Bad
const a = 1
const b = 2
var a = 1
var b = 2
type Area float64
type Volume float64
Good
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
)
仅将相关的声明放在一组,不要将不相关的声明放在一组
Bad
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
ENV_VAR = "MY_ENV"
)
Good
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const ENV_VAR = "MY_ENV"
分组使用的位置没有限制,例如:你可以在函数内部使用它们
Bad
func f() string {
var red = color.New(0xff0000)
var green = color.New(0x00ff00)
var blue = color.New(0x0000ff)
...
}
Good
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
...
}
import分组
导入应该分为两组:
- 标准库
- 其他库
默认情况下,这是goimports应用的分组
Bad
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
Good
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
)
包名
当命名包时,请按下面规则选择一个名称:
- 全部小些,没有大写或下划线;
- 大多数使用命名导入的情况下,不需要重命名;
- 简短而简洁。请记住,在每个使用的地方都完整标识了该名称;
- 不用复数。例如
net/url
,而不是net/urls
; - 不要用 “common”,“util”,“shared” 或 “lib”。这些是不好的,信息量不足的名称;
导入别名
如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
在所有其他情况下,除非导入之间有直接冲突,否则应避免导入别名
Bad
import (
"fmt"
"os"
nettrace "golang.net/x/trace"
)
Good
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
)
函数分组与顺序
- 函数应按粗略的调用顺序排序
- 同一文件中的函数应按接收者分组
因此,导出的函数应先出现在文件中,放在struct
,const
,var
定义的后面;
在定义类型之后,但在接收者的其余方法之前,可能会出现一个newXYZ()
/NewXYZ()
由于函数是按接收者分组的,因此普通工具函数应在文件末尾出现
Bad
func (s *something) Cost() {
return calcCost(s.weights)
}
type something struct{ ... }
func calcCost(n int[]) int {...}
func (s *something) Stop() {...}
func newSomething() *something {
return &something{}
}
Good
type something struct{ ... }
func newSomething() *something {
return &something{}
}
func (s *something) Cost() {
return calcCost(s.weights)
}
func (s *something) Stop() {...}
func calcCost(n int[]) int {...}
减少嵌套
代码应通过尽可能先处理错误情况/特殊情况,并且尽早返回或继续循环来减少嵌套。减少嵌套多个级别的代码的代码量。
Bad
for _, v := range data {
if v.F1 == 1 {
v = process(v)
if err := v.Call(); err == nil {
v.Send()
} else {
return err
}
} else {
log.Printf("Invalid v: %v", v)
}
}
Good
for _, v := range data {
if v.F1 != 1 {
log.Printf("Invalid v: %v", v)
continue
}
v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
}
不必要的else
如果在if的两个分支中都设置了变量,则可以将其替换为单个if
Bad
var a int
if b {
a = 100
} else {
a = 10
}
Good
a := 10
if b {
a = 100
}
顶层变量声明
在顶层,使用标准var
关键字。请勿指定类型,除非它与表达式的类型不同
Bad
var _s string = F()
func F() string { return "A" }
Good
var _s = F()
// 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型
// 还是那种类型
func F() string { return "A" }
如果表达式的类型与所需的类型不完全匹配,请指定类型
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F 返回一个 myError 类型的实例,但是我们要 error 类型
对于未导出的顶层常量和变量,使用_作为前缀
在未导出的顶级vars
和consts
,前面加上前缀_,以使它们在使用时明确表示它们是全局符号
例外:未导出的错误,应以err
开头
基本依据:顶级变量和常量具有包范围作用域,使用通用名称可能很容易在其他文件中意外使用错误的值
Bad
// foo.go
const (
defaultPort = 8080
defaultUser = "user"
)
// bar.go
func Bar() {
defaultPort := 9090
...
fmt.Println("Default port", defaultPort)
// We will not see a compile error if the first line of
// Bar() is deleted.
}
Good
// foo.go
const (
_defaultPort = 8080
_defaultUser = "user"
)
结构体中的嵌入
嵌入式类型应位于结构体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与常规字段分隔开
Bad
type Client struct {
version int
http.Client
}
Good
type Client struct {
http.Client
version int
}
使用字段名初始化结构体
初始化结构体时,几乎始终应该指定字段名称。现在由go vet
强制执行
Bad
k := User{"John", "Doe", true}
Good
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
例外:如果有3个或更少的字段,则可以在测试表中省略字段名称
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
本地变量声明
如果将变量明确设置为某个值,则应使用短变量声明形式:=
Bad
var s = "foo"
Good
s := "foo"
但是,在某些情况下,var
使用关键字时默认值会更清晰。例如,声明空切片
Bad
func f(list []int) {
filtered := []int{}
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}
Good
func f(list []int) {
var filtered []int
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
}
nil是一个有效的slice
nil
是一个有效的长度为0的slice,这意味着,
- 您不应明确返回长度为0的切片,应该返回
nil
来代替
Bad
if x == "" {
return []int{}
}
Good
if x == "" {
return nil
}
- 要检查切片是否为空,请始终使用
len(s) == 0
,而非nil
Bad
func isEmpty(s []string) bool {
return s == nil
}
Good
func isEmpty(s []string) bool {
return len(s) == 0
}
- 零值切片(用
var
声明的切片)可立即使用,无需调用make()
创建
Bad
nums := []int{}
// or, nums := make([]int)
if add1 {
nums = append(nums, 1)
}
if add2 {
nums = append(nums, 2)
}
Good
var nums []int
if add1 {
nums = append(nums, 1)
}
if add2 {
nums = append(nums, 2)
}
小变量作用域
如果有可能,尽量缩小变量作用范围,除非它与减少嵌套的规则冲突
Bad
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
return err
}
Good
if err := ioutil.WriteFile(name, data, 0644); err != nil {
return err
}
如果需要在if之外使用函数调用的结果,则不应尝试缩小范围
Bad
if data, err := ioutil.ReadFile(name); err == nil {
err = cfg.Decode(data)
if err != nil {
return err
}
fmt.Println(cfg)
return nil
} else {
return err
}
Good
data, err := ioutil.ReadFile(name)
if err != nil {
return err
}
if err := cfg.Decode(data); err != nil {
return err
}
fmt.Println(cfg)
return nil
避免参数语义不明确
函数调用中的意义不明确的参数
可能会损害可读性。当参数名称的含义不明确时,请为参数添加c样式注释(/*...*/
)
Bad
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true, true)
Good
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */)
对于上面的示例代码,还有一种更好的处理方式,将上面的bool
类型换成自定义类型。将来,该参数可以支持不仅仅局限于两个状态(true/false)
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady = iota + 1
StatusDone
// Maybe we will have a StatusInProgress in the future.
)
func printInfo(name string, region Region, status Status)
使用原始字符串字面值,避免转义
Go支持使用原始字符串字面值
,也就是"`"来表示原生字符串,在需要转义的场景下,我们应该尽量使用这种方案来替换。
可以跨越多行并包含引号,使用这些字符串可以避免更难阅读的手工转义的字符串
Bad
wantError := "unknown name:.test."
Good
wantError := `unknown error:"test"`
初始化Struct引用
在初始化结构引用时,请使用&T{}
代替new(T)
,以使其与结构体初始化一致
Bad
sval := T{Name: "foo"}
// inconsistent
sptr := new(T)
sptr.Name = "bar"
Good
sval := T{Name: "foo"}
sptr := &T{Name: "bar"}
初始化Maps
对于空map请使用make(..)
初始化,并且map是通过编程方式填充的。这使得map初始化在表现上不同于声明,并且它还可以方便地在make后添加大小提示。
Bad
声明和初始化看起来非常相似的
var (
// m1 读写安全;
// m2 在写入时会 panic
m1 = map[T1]T2{}
m2 map[T1]T2
)
Good
声明和初始化看起来差别非常大
var (
// m1 读写安全;
// m2 在写入时会 panic
m1 = make(map[T1]T2)
m2 map[T1]T2
)
字符串string format
如果你为Printf
-style 函数声明格式字符串,请将格式化字符串放在外面,并将其设置为const
常量。
这有助于go vet
对格式字符串执行静态分析。
Bad
msg := "unexpected values %v, %v."
fmt.Printf(msg, 1, 2)
Good
const msg = "unexpected values %v, %v."
fmt.Printf(msg, 1, 2)