概述

当在 GO 中遇到错误时你会怎么做?

处理错误并不简单。在讨论功能性需求时,很少考虑错误处理需求,但是错误处理是软件开发的一个重要部分。

在 GO 中,错误条件以方法返回值的形式返回。在我看来,将错误条件作为主流程的一部分是很有用的 – 它让开发人员在编写功能代码时承担处理错误的责任。这种范例与其他编程语言(如 Java )所提供的非常不同 – 其中异常是完全不同的流程。虽然这种不同的风格使代码更具可读性,但也带来了新的挑战。

本文讨论了六种处理错误、重试和可服务性的技术。虽然很少有想法是琐碎的,但其他想法并不那么受欢迎。

因此,让我们从列表开始!

1 向左对齐

处理错误的最佳策略是检查错误并立即从函数返回。在一个函数中有多个错误返回语句是可以的 – 事实上,这是明智的选择。[1]

if err == nil
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Handling Happy case first - leading to nested if checks...
func example() error {
err := somethingThatReturnsError()
if err == nil {
//Happy processing
err = somethingElseThatReturnsError()
if err == nil {
//More Happy processing
} else {
return err
}
} else {
return err
}
}

上述逻辑可以通过向左对齐逻辑来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
func ABetterExample() error {
err := somethingThatReturnsError()
if err != nil {
return err
}

// Happy processing
err = somethingElseThatReturnsError()
if err != nil {
return err
}
// More Happy processing
}

2 重试可恢复错误

很少有可恢复的错误值得重试 – 网络故障、IO 操作等都可以通过简单的重试恢复。

下面的包可以帮助解决重试带来的麻烦。
package backoff

1
2
3
4
5
6
7
8
9
10
// An operation that may fail.
operation := func() error {
return nil // or an error
}

err := Retry(operation, NewExponentialBackOff())
if err != nil {
// Handle error.
return
}

指数回退意味着重试间隔呈指数增长 – 对于大多数网络 /IO 故障来说,这是一个明智的选择。

3 包装错误

默认的错误包是有限的 – 错误上下文的详细信息经常会丢失。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func testingError2() error {
return errors.New("New Error")
}
func testingError(accountNumber string) error {
err := testingError2()
if err != nil {
return err
}
return nil
}
func main() {
err := testingError("Acct1")
logrus.Error("Error occurred", fmt.Sprintf("%+v", err))
}
Acct1testingErrroraccountNumber
github.Com/pkg/errorserrors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func testingError2() error {
return errors.New("New Error")
}
func testingError(accountNumber string) error {
err := testingError2()
if err != nil {
return errors.Wrap(err, "Error occurred while processing Card Number "+accoutNumber)
}
return nil
}
func main() {
err := testingError("Acct1")
logrus.Error("Error occurred", fmt.Sprintf("%+v", err))
}
github.com/pkg/errorserrors.Unwraperrors.Is

4 日志策略

Golang 的默认包日志不提供使用日志记录级别进行日志记录功能。这里有一些其他的选择:

LogrusZap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func example(accountNumber string) error {
logrus.SetFormatter(&logrus.JSONFormatter{})
ctxFields := logrus.Fields{
"accountNumber": accountNumber,
"appname": "my-app",
}
//Happy processing
err := errors.New("Some test error while doing happy processing")
if err != nil {
logrus.WithFields(ctxFields).WithError(err).Error("ErrMsg")
return err
}
return nil
}

结构化日志输出如下:

1
{"accountNumber":"ABC","appname":"my-app","error":"Some test error while doing happy processing","level":"error","msg":"ErrMsg","time":"2009-11-10T23:00:00Z"}
github.com/pkg/errors
1
logrus.Error("Error occurred", fmt.Sprintf("%+v", err))

你会得到一个错误堆栈跟踪如下:

1
2
3
4
5
6
7
8
9
10
main.testingError2
/home/nayars/go/src/github.com/nayarsn/temp.go:12
main.testingError
/home/nayars/go/src/github.com/nayarsn/temp.go:25
main.main
/home/nayars/go/src/github.com/nayarsn/temp.go:39
runtime.main
/usr/lib/go-1.15/src/runtime/proc.go:204
runtime.goexit
/usr/lib/go-1.15/src/runtime/asm_amd64.s:1374
Zap

5 错误检查

将错误视为值是好的 – 它是明确的,而明确的有很多意义。但它也可以为开发人员提供跳过的机会。例如:

1
2
3
4
5
6
func testingError(accoutNumber string) error {
var err error
_ = errors.New("errors.New with _"
errors.New("errors.New not capturing return")
return err
}
errors.New

幸运的是,有一个 linter 实用程序可以帮助您。
kisielk/errcheck

一旦你安装了 linter,你可以简单地做以下事情:

1
errcheck -blank ./...

你会得到这样的输出:

1
2
temp.go:16:2:   _ = errors.New("Error capturing return using _")
temp.go:18:12: errors.New("Error not capturing return")

这可以作为 CI/CD 流程的一部分,以确保应用程序开发人员不会错过这一部分。

errchec

6 多个错误

go routine

这里有一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func step1() error {
return errors.New("Step1")
}
func step2() error {
return errors.New("Step2")
}
func main() {
var result error
if err := step1(); err != nil {
result = multierror.Append(result, err)
}
if err := step2(); err != nil {
result = multierror.Append(result, err)
}

fmt.Println(multierror.Flatten(result))
}
go routines

总结

我知道上述列表并非全部。对于你们中的一些人来说,这可能是微不足道的 – 但希望对你们中的一些人来说,这有助于你们掌握错误处理技术。

参考

微信订阅号