我在《Go 错误处理指北:pkg/errors 源码解读》 一文中对 pkg/errors 包源码做了详细的讲解,不过却没有在用法上给出过多建议,本问就来讲解一下。
pkg/errors 是由 Dave Cheney 所开发的,是目前 Go 错误处理的最优解。
记录错误调用链
我们可以在错误调用链中,使用 pkg/errors 提供的 errors.Wrap 方法为错误附加一些信息,以此来记录链路调用过程。
示例如下:
package main
import (
"fmt"
"github.com/pkg/errors"
)
func Foo() error {
return errors.New("foo error")
}
func Bar() error {
err := Foo()
if err != nil {
return errors.Wrap(err, "bar")
}
return nil
}
func main() {
err := Bar()
if err != nil {
fmt.Printf("err: %s\n", err)
}
}
执行示例代码,得到输出如下:
$ go run main.go
err: bar: foo error
记录错误堆栈
附加错误信息还不够,pkg/errors 包最大的好处是可以记录错误堆栈。
修改 main 函数的错误处理,只需要将 fmt.Printf 中格式化错误的动词从 %s 改成 %+v 即可:
func main() {
err := Bar()
if err != nil {
fmt.Printf("err: %+v\n", err)
}
}
执行示例代码,得到输出如下:
$ go run main.go
err: foo error
main.Foo
/go/blog-go-example/error/handling-error/pkg-errors/main.go:32
main.Bar
/go/blog-go-example/error/handling-error/pkg-errors/main.go:36
main.main
/go/blog-go-example/error/handling-error/pkg-errors/main.go:44
runtime.main
/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/runtime/proc.go:272
runtime.goexit
/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/runtime/asm_arm64.s:1223
bar
main.Bar
/go/blog-go-example/error/handling-error/pkg-errors/main.go:38
main.main
/go/blog-go-example/error/handling-error/pkg-errors/main.go:44
runtime.main
/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/runtime/proc.go:272
runtime.goexit
/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/runtime/asm_arm64.s:1223
可以看到,错误从产生开始,整个调用链堆栈信息都被记录了下来。
但是这里存在重复的问题,错误调用链被打印了两次。这其实是因为 pkg/errors 包提供的 errors.New 函数本身在构造错误时就已经记录了堆栈信息,而 errors.Wrap 又记录了一遍。
所以,如果错误是通过 errors.New 构造的,调用链中间不应该再次使用 errors.Wrap 附加错误信息,而应该使用 errors.WithMessage。
修改 Bar 函数如下:
func Bar() error {
err := Foo()
if err != nil {
return errors.WithMessage(err, "bar")
}
return nil
}
执行示例代码,得到输出如下:
$ go run main.go
err: foo error
main.Foo
/go/blog-go-example/error/handling-error/pkg-errors/main.go:32
main.Bar
/go/blog-go-example/error/handling-error/pkg-errors/main.go:36
main.main
/go/blog-go-example/error/handling-error/pkg-errors/main.go:44
runtime.main
/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/runtime/proc.go:272
runtime.goexit
/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/runtime/asm_arm64.s:1223
bar
现在记录的错误堆栈就正常了。
不要做冗余的错误检查
其实 pkg/errors 包提供了更方便的使用方法。
我们无需编写这种代码:
func Bar() error {
err := Foo()
if err != nil {
return errors.WithMessage(err, "bar")
}
return nil
}
可以直接去掉那冗余的错误检查:
func Bar() error {
err := Foo()
return errors.WithMessage(err, "bar")
}
这不对执行结果造成任何影响。
我们无需判断 err 是否为 nil,因为 pkg/errors 内部的方法帮我们做好了这项检查:
func WithMessage(err error, message string) error {
if err == nil {
return nil
}
return &withMessage{
cause: err,
msg: message,
}
}
对于 errors.Wrap/errors.WithStack 同样如此。
Sentinel error 处理
因为 pkg/errors 包提供的 errors.Wrap/errors.WithStack/errors.WithMessage 这三个方法都会返回新的错误,所以默认情况下 Sentinel error 相等性判断就会失效。
不过 pkg/errors 包考虑到了这点,提供了 errors.Cause 方法可以得到一个错误的根因。
示例如下:
func Foo() error {
return io.EOF
}
func Bar() error {
err := Foo()
return errors.WithMessage(err, "bar")
}
func main() {
err := Bar()
if err != nil {
if errors.Cause(err) == io.EOF {
fmt.Println("EOF err")
return
}
fmt.Printf("err: %+v\n", err)
}
return
}
执行示例代码,得到输出如下:
$ go run main.go
EOF err
总结
可以发现,pkg/errors 包充分考虑了人类和程序对错误的不同处理。
pkg/errors 包可以非常方便的向一个已有错误添加新的上下文,错误堆栈可以方便我们程序员排查问题,errors.Cause 获取错误根因的方法,可以方便程序中对错误进行相等性检查。
如果我们的代码中全局都在使用 pkg/errors 包,那么通过 errors.New/errors.Errorf 构造的错误天然就已经携带了错误堆栈信息。
通常在调用链中间过程直接返回底层错误即可,如果想要附加信息,则可以使用 errors.WithMessage,不要使用 errors.Wrap/errors.WithStack 以免造成堆栈信息的重复。
如果与标准库或来自第三方的代码包进行交互,可以考虑使用 errors.Wrap/errors.WithStack 在原错误基础上建立堆栈跟踪。
在错误处理调用链顶层,可以使用 %+v 来记录包含足够详细信息的错误。
本文示例源码我都放在了 GitHub 中,欢迎点击查看。
希望此文能对你有所启发。
联系我
- 公众号:Go编程世界
- 微信:jianghushinian
- 邮箱:jianghushinian007@outlook.com
- 博客:jianghushinian.cn