Go Best Practices

错误包装(Error Wrapping)

一个(函数/方法)调用失败时,有三种主要的错误传播方式:

如果没有要添加的其他上下文,并且您想要维护原始错误类型,则返回原始错误。

添加上下文,使用”pkg/errors”.Wrap以便错误消息提供更多上下文,”pkg/errors”.Cause可用于提取原始错误。

使用fmt.Errorf,如果调用者不需要检测或处理的特定错误情况。

建议在可能的地方添加上下文,以使您获得诸如“调用服务foo:连接被拒绝”之类的更有用的错误,而不是诸如“连接被拒绝”之类的模糊错误。

在将上下文添加到返回的错误时,请避免使用“ failed to”之类的短语来保持上下文简洁,这些短语会陈述明显的内容,并随着错误在堆栈中的渗透而逐渐堆积:

Bad

s, err := store.New()

if err != nil {

return fmt.Errorf(

    "failed to create new store: %s", err)

}

failed to x: failed to y: failed to create new store: the error

vs.

Good

s, err := store.New()

if err != nil {

return fmt.Errorf(

    "new store: %s", err)

}

x: y: new store: the error

但是,一旦将错误发送到另一个系统,就应该明确消息是错误消息(例如使用err标记,或在日志中以”Failed”为前缀)。

另请参见Don’t just check errors, handle them gracefully.

处理类型断言失败

类型断言的单个返回值形式针对不正确的类型将产生panic。因此,请始终使用“comma ok”的惯用法。

Bad

t := i.(string)

vs.

Good

t, ok := i.(string)

if !ok {

// 优雅地处理错误

}

不要 panic

在生产环境中运行的代码必须避免出现panic。panic是级联失败的主要根源 。如果发生错误,该函数必须返回错误,并允许调用方决定如何处理它。

Bad

func foo(bar string) {

if len(bar) == 0 {

panic("bar must not be empty")

}

// …

}

func main() {

if len(os.Args) != 2 {

fmt.Println("USAGE: foo <bar>")

os.Exit(1)

}

foo(os.Args[1])

}

vs.

Good

func foo(bar string) error {

if len(bar) == 0

return errors.New("bar must not be empty")

}

// …

return nil

}

func main() {

if len(os.Args) != 2 {

fmt.Println("USAGE: foo <bar>")

os.Exit(1)

}

if err := foo(os.Args[1]); err != nil {

panic(err)

}

}

panic/recover不是错误处理策略。仅当发生不可恢复的事情(例如:nil引用)时,程序才必须panic。程序初始化是一个例外:程序启动时应使程序中止的不良情况可能会引起panic。

var _statusTemplate = template.Must(template.New(“name”).Parse("_statusHTML"))

即便是在test中,也优先使用t.Fatal或t.FailNow来标记test是失败的,而不是panic。

Bad

// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", “test”)

if err != nil {

panic(“failed to set up test”)

}

vs.

Good

// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", “test”)

if err != nil {

t.Fatal(“failed to set up test”)

}

使用go.uber.org/atomic

使用sync/atomic包的原子操作对原始类型(int32,int64等)进行操作(译注:指atomic包的方法名中均使用原始类型名,如SwapInt32等),因此很容易忘记使用原子操作来读取或修改变量。

go.uber.org/atomic通过隐藏基础类型为这些操作增加了类型安全性。此外,它包括一个方便的atomic.Bool类型。

Bad

type foo struct {

running int32 // atomic

}

func (f* foo) start() {

if atomic.SwapInt32(&f.running, 1) == 1 {

 // already running…

 return

}

// start the Foo

}

func (f *foo) isRunning() bool {

return f.running == 1 // race!

}

vs.

Good

type foo struct {

running atomic.Bool

}

func (f *foo) start() {

if f.running.Swap(true) {

 // already running…

 return

}

// start the Foo

}

func (f *foo) isRunning() bool {

return f.running.Load()

}

三. 性能

性能方面的特定准则,适用于热路径。

优先使用strconv而不是fmt

将原语转换为字符串或从字符串转换时,strconv速度比fmt快。

Bad

for i := 0; i < b.N; i++ {

s := fmt.Sprint(rand.Int())

}

BenchmarkFmtSprint-4 143 ns/op 2 allocs/op

vs.

Good

for i := 0; i < b.N; i++ {

s := strconv.Itoa(rand.Int())

}

BenchmarkStrconv-4 64.2 ns/op 1 allocs/op

避免字符串到字节的转换

不要反复从固定字符串创建字节slice。相反,请执行一次转换并捕获结果。

Bad

for i := 0; i < b.N; i++ {

w.Write([]byte(“Hello world”))

}

BenchmarkBad-4 50000000 22.2 ns/op

vs.

Good

data := []byte(“Hello world”)

for i := 0; i < b.N; i++ {

w.Write(data)

}

BenchmarkGood-4 500000000 3.25 ns/op