Go Best Practices

结构体中的嵌入

嵌入式类型(例如mutex)应位于结构体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与常规字段分隔开。

Bad

type Client struct {

version int

http.Client

}

vs.

Good

type Client struct {

http.Client

version int

}

使用字段名初始化结构体

初始化结构体时,几乎始终应该指定字段名称。现在由go vet强制执行。

Bad

k := User{“John”, “Doe”, true}

vs.

Good

k := User{

FirstName: "John",

LastName: "Doe",

Admin: true,

}

例外:如果有3个或更少的字段,则可以在测试表中省略字段名称。

tests := []struct{

}{

op Operation

want string

}{

{Add, “add”},

{Subtract, “subtract”},

}

本地变量声明

如果将变量明确设置为某个值,则应使用短变量声明形式(:=)。

Bad

var s = “foo”

vs.

Good

s := “foo”

但是,在某些情况下,var 使用关键字时默认值会更清晰。例如,声明空切片。

Bad

func f(list []int) {

filtered := []int{}

for _, v := range list {

if v > 10 {

  filtered = append(filtered, v)

}

}

}

vs.

Good

func f(list []int) {

var filtered []int

for _, v := range list {

if v > 10 {

  filtered = append(filtered, v)

}

}

}

nil是一个有效的slice

nil是一个有效的长度为0的slice,这意味着:

您不应明确返回长度为零的切片。返回nil 来代替。

Bad

if x == “” {

return []int{}

}

vs.

Good

if x == “” {

return nil

}

要检查切片是否为空,请始终使用len(s) == 0。不要检查 nil。

Bad

func isEmpty(s []string) bool {

return s == nil

}

vs.

Good

func isEmpty(s []string) bool {

return len(s) == 0

}

零值切片可立即使用,无需调用make创建。

Bad

nums := []int{}

// or, nums := make([]int)

if add1 {

nums = append(nums, 1)

}

if add2 {

nums = append(nums, 2)

}

vs.

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

}

vs.

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

}

vs.

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)

vs.

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””

vs.

Good

wantError := unknown error:"test"

初始化结构体引用

在初始化结构引用时,请使用&T{}代替new(T),以使其与结构体初始化一致。

Bad

sval := T{Name: “foo”}

// 不一致

sptr := new(T)

sptr.Name = “bar”

vs.

Good

sval := T{Name: “foo”}

sptr := &T{Name: “bar”}

格式化字符串放在Printf外部

如果你为Printf-style函数声明格式字符串,请将格式化字符串放在外面,并将其设置为const常量。

这有助于go vet对格式字符串执行静态分析。

Bad

msg := “unexpected values %v, %v\n”

fmt.Printf(msg, 1, 2)

vs.

Good

const msg = “unexpected values %v, %v\n”

fmt.Printf(msg, 1, 2)

命名Printf样式的函数

声明Printf-style函数时,请确保go vet可以检测到它并检查格式字符串。

这意味着您应尽可能使用预定义的Printf-style函数名称。go vet将默认检查这些。有关更多信息,请参见Printf系列

如果不能使用预定义的名称,请以f结束选择的名称:Wrapf,而不是Wrap。go vet可以要求检查特定的Printf样式名称,但名称必须以f结尾。

$ go vet -printfuncs = wrapf,statusf

另请参阅”go vet:Printf家族检查“。

五. 模式

测试表

在核心测试逻辑重复时,将表驱动测试与子测试一起使用,以避免重复代码。

Bad

// func TestSplitHostPort(t *testing.T)

host, port, err := net.SplitHostPort(“192.0.2.0:8000”)

require.NoError(t, err)

assert.Equal(t, “192.0.2.0”, host)

assert.Equal(t, “8000”, port)

host, port, err = net.SplitHostPort(“192.0.2.0:http”)

require.NoError(t, err)

assert.Equal(t, “192.0.2.0”, host)

assert.Equal(t, “http”, port)

host, port, err = net.SplitHostPort(":8000")

require.NoError(t, err)

assert.Equal(t, “”, host)

assert.Equal(t, “8000”, port)

host, port, err = net.SplitHostPort(“1:8”)

require.NoError(t, err)

assert.Equal(t, “1”, host)

assert.Equal(t, “8”, port)

vs.

Good

// func TestSplitHostPort(t *testing.T)

tests := []struct{

give string

wantHost string

wantPort string

}{

{

give:     "192.0.2.0:8000",

wantHost: "192.0.2.0",

wantPort: "8000",

},

{

give:     "192.0.2.0:http",

wantHost: "192.0.2.0",

wantPort: "http",

},

{

give:     ":8000",

wantHost: "",

wantPort: "8000",

},

{

give:     "1:8",

wantHost: "1",

wantPort: "8",

},

}

for _, tt := range tests {

t.Run(tt.give, func(t *testing.T) {

host, port, err := net.SplitHostPort(tt.give)

require.NoError(t, err)

assert.Equal(t, tt.wantHost, host)

assert.Equal(t, tt.wantPort, port)

})

}

测试表使向错误消息添加上下文,减少重复的逻辑以及添加新的测试用例变得更加容易。

我们遵循这样的约定:将结构体切片称为tests。 每个测试用例称为tt。此外,我们鼓励使用give和want前缀说明每个测试用例的输入和输出值。

tests := []struct{

give string

wantHost string

wantPort string

}{

// …

}

for _, tt := range tests {

// …

}

功能选项

功能选项是一种模式,您可以在其中声明一个不透明Option类型,该类型在某些内部结构中记录信息。您接受这些选项的可变编号,并根据内部结构上的选项记录的全部信息采取行动。

将此模式用于您需要扩展的构造函数和其他公共API中的可选参数,尤其是在这些功能上已经具有三个或更多参数的情况下。

Bad

// package db

func Connect(

addr string,

timeout time.Duration,

caching bool,

) (*Connection, error) {

// …

}

// Timeout and caching must always be provided,

// even if the user wants to use the default.

db.Connect(addr, db.DefaultTimeout, db.DefaultCaching)

db.Connect(addr, newTimeout, db.DefaultCaching)

db.Connect(addr, db.DefaultTimeout, false /* caching */)

db.Connect(addr, newTimeout, false /* caching */)

vs.

Good

type options struct {

timeout time.Duration

caching bool

}

// Option overrides behavior of Connect.

type Option interface {

apply(*options)

}

type optionFunc func(*options)

func (f optionFunc) apply(o *options) {

f(o)

}

func WithTimeout(t time.Duration) Option {

return optionFunc(func(o *options) {

o.timeout = t

})

}

func WithCaching(cache bool) Option {

return optionFunc(func(o *options) {

o.caching = cache

})

}

// Connect creates a connection.

func Connect(

addr string,

opts …Option,

) (*Connection, error) {

options := options{

timeout: defaultTimeout,

caching: defaultCaching,

}

for _, o := range opts {

o.apply(&options)

}

// …

}

// Options must be provided only if needed.

db.Connect(addr)

db.Connect(addr, db.WithTimeout(newTimeout))

db.Connect(addr, db.WithCaching(false))

db.Connect(

addr,

db.WithCaching(false),

db.WithTimeout(newTimeout),

)

还可以参考下面资料:

Self-referential functions and the design of options

Functional options for friendly APIs