Go错误处理实践
Golang的错误处理一直备受诟病,if err != nil
满天飞,甚至可以说有点简陋~
因此需要了解学习下怎么的做法才是比较好的错误实践。
一.标准库的error
Go创始人之一的Rob Pike对error的设计理念是"Errors are values",他认为error就是一个值,跟普通函数的返回值一样,地位相等,并没有特别之处。
因此代码流程中会反复出现以下代码也是能理解的:
if err != nil {
return err
}
但是因为在Go语言中绝大多数时候error的处理都只需要判断非空返回,因此会造成冗长又重复的error处理,导致被诟病。
1. error类型定义
Go标准库中error类型是一个接口类型,在src/builtin/builtin.go
文件中定义:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
只要实现了Error()
方法的类型都可以作为error返回,它将错误消息以字符串的形式返回。
2. error创建
可以通过两个方法来创建一个error:
errors
包New方法:func New(text string) error
fmt
包Errorf方法:func Errorf(format string, a ...interface{}) error
可以看下errors.New()
方法的源码:
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
从源码里可以看出,New方法返回的是一个&errorString{text}
的指针,errorString类型是实现了error接口的。
在Go语言中,指针的等值比较是依据地址的,所以,即使两个errors.New
的内容相同,等值比较也是不一样的。
func main() {
err1 := errors.New("db error")
err2 := errors.New("db error")
fmt.Println(err1 == err2) // 返回false
}
3. error使用
通过上面两种方法都可以创建一个error,其中接口方法Error() string
返回的就是上面方法中传入的字符串参数,对于一般的简单错误,这样子的操作对于小工程来说其实已经足够了。
比如:
func main() {
_, err := foo()
if err != nil {
// TODO:处理错误
}
}
但是一旦工程大了,有多个错误要处理时候,就会演变成这样:
func main() {
_, err := foo()
if err != nil {
// TODO:处理错误
}
_, err := foo()
if err != nil {
// TODO:处理错误
}
_, err := foo()
if err != nil {
// TODO:处理错误
}
...
}
这样一来,调用多个方法,就要处理多个错误,而且是错误处理,就不只是简单的return err
就完了,项目代码是层层调用的,直接返回err会导致错误无法准确定位。
比如一个底层打开文件出错,到了顶层就只能获取到类似这样子的错误:No such file or directory
,但并不清楚哪里出问题。
4. 弊端
对于Go标准库的error设计,从上面的使用可以感受到有以下几个弊端:
- error会大量穿插在代码中,基本返回值有error的,都需要进行一次
if err != nil
判断,整个代码下来占据大量,可读性比较差 - 很多重复的
if err != nil
片段,没法简化 - 简单的使用
return err
不能满足所有的场景
二.Go1.13的改进
在Go1.13中,对errors包新增了一些接口,同时也给fmt包的Errorf函数格式化增加了一个新的格式符:%w
。
1. Go1.13前的fmt.Errorf
在Go1.13前,通过fmt.Errorf
可以创建一个error,比如:
if err != nil {
return fmt.Errorf("main.go error: %v", err)
}
从上面可以知道,error接口就只有一个Error()方法,返回的是错误信息的字符串形式,因此这里使用fmt.Errorf
包装生成一个新的error,原有的error类型将会丢失,上层无法感知到错误最终来源。
2. Go1.13的变化
- Wrap
1.13版本开始支持了error的包裹(wrap),通过fmt.Errorf
加上%w
格式符来嵌套一个error
示例如下:
func data() error {
return errors.New("db error")
}
func biz() error {
if err := data(); err != nil {
return fmt.Errorf("biz error: %w", err)
}
return nil
}
func service() error {
if err := biz(); err != nil {
return fmt.Errorf("service error: %w", err)
}
return nil
}
func main() {
err := service()
fmt.Println(err)
}
// 输出: service error: biz error: db error
- Unwrap
与wrap对应的是Unwrap,在Go1.13中errors包新增了Unwrap接口:func Unwrap(err error) error
,其源码如下:
// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
Unwrap将嵌套的error解包出来,每调一次Unwrap就剥离一层,对err进行断言,看看它是否有实现Unwrap方法,如果有则调用它的Unwrap方法,没有则返回nil。
因此在Go1.13以后,我们自定义error的话,除了要实现Error() string
方法外,最好还要实现Unwrap() error
方法,默认返回自身。
比如上面Wrap的示例,进行Unwrap:
func main() {
err := service()
err1 := errors.Unwrap(err)
err2 := errors.Unwrap(err1)
fmt.Println(err)
fmt.Println(err1)
fmt.Println(err2)
// 输出:
// service error: biz error: db error
// biz error: db error
// db error
}
可以查看Errorf
的源码,其实现了Unwrap方法,内部是用了一个wrapError
结构来包装原始error的:
// Errorf formats according to a format specifier and returns the string as a
// value that satisfies error.
//
// If the format specifier includes a %w verb with an error operand,
// the returned error will implement an Unwrap method returning the operand. It is
// invalid to include more than one %w verb or to supply it with an operand
// that does not implement the error interface. The %w verb is otherwise
// a synonym for %v.
func Errorf(format string, a ...any) error {
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a)
s := string(p.buf)
var err error
if p.wrappedErr == nil {
err = errors.New(s)
} else {
err = &wrapError{s, p.wrappedErr}
}
p.free()
return err
}
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
::: warning 注意
当前go版本中,fmt.Errorf只支持嵌套一层error,只能有一个%w。
在Go1.20版本中将会支持wrap multiple errors
:::
- Is和As
Go1.13还新增Is和As两个接口:
func Is(err, target error) bool
func As(err error, target any) bool
Is用于判断err和target是不是同一类型,或者err嵌套的error中有没有和target是同一类型的,如果有,则返回true。
源码如下:
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
// 无限循环,比较 err 以及嵌套的 error
for {
if isComparable && err == target {
return true
}
// 调用 error 的 Is 方法,这里可以自定义实现
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// 返回被嵌套的下一层的 error
if err = Unwrap(err); err == nil {
return false
}
}
}
使用示例,还是上面的例子:
var Err1 = errors.New("db error")
func data() error {
return Err1
}
func biz() error {
if err := data(); err != nil {
return fmt.Errorf("biz error: %w", err)
}
return nil
}
func service() error {
if err := biz(); err != nil {
return fmt.Errorf("service error: %w", err)
}
return nil
}
func main() {
err := service()
err1 := errors.Unwrap(err)
err2 := errors.Unwrap(err1)
fmt.Println(errors.Is(err, Err1)) // true
fmt.Println(errors.Is(err, err1)) // true
fmt.Println(errors.Is(err, err2)) // true
fmt.Println(errors.Is(err, errors.New("db error"))) // false
fmt.Println(errors.Is(err1, err2)) // true
}
As将err错误链中找到的和target相同的error,并将target设置为该error类型。(类似使用断言)
源码如下:
func As(err error, target interface{}) bool {
// target 不能为 nil
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
// target 必须是一个非空指针
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
// 保证 target 是一个接口类型或者实现了 Error 接口
if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
for err != nil {
// 使用反射判断是否可被赋值,如果可以就赋值并且返回true
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
// 调用 error 自定义的 As 方法,实现自己的类型断言代码
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
// 不断地 Unwrap,一层层的获取嵌套的 error
err = Unwrap(err)
}
return false
}
使用示例:
type MyError struct {
msg string
err error
}
func (e *MyError) Error() string {
return e.msg
}
func (e *MyError) Unwrap() error {
return e.err
}
func data() error {
return &MyError{
msg: "db error",
}
}
func biz() error {
if err := data(); err != nil {
return fmt.Errorf("biz error: %w", err)
}
return nil
}
func service() error {
if err := biz(); err != nil {
return fmt.Errorf("service error: %w", err)
}
return nil
}
func main() {
err := service()
var myErr *MyError
if ok := errors.As(err, &myErr); ok {
fmt.Println(myErr) // 输出:db error
}
}
三.第三方库pkg/errors
实质上Go1.13就是借鉴了pkg/errors的,但pkg/errors包的Wrap函数会携带出错时的上下文,也就是调用的堆栈信息。
pkg/errors也是Uber Go 语言编码规范中推荐使用的。
这个包提供以下几个主要接口:
func New(message string) error
func Wrap(err error, message string) error
func Cause(err error) error
- 原始error使用errors.New
这个与标准库的最大区别就是增加了调用的stack信息,这样子error就不再只是简单的一条字符串,还携带了堆栈信息,其源码如下:
// New returns an error with the supplied message.
// New also records the stack trace at the point it was called.
func New(message string) error {
return &fundamental{
msg: message,
stack: callers(),
}
}
// fundamental is an error that has a message and a stack, but no caller.
type fundamental struct {
msg string
*stack
}
func (f *fundamental) Error() string { return f.msg }
func (f *fundamental) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
io.WriteString(s, f.msg)
f.stack.Format(s, verb)
return
}
fallthrough
case 's':
io.WriteString(s, f.msg)
case 'q':
fmt.Fprintf(s, "%q", f.msg)
}
}
从源码可以看到,fundamental类型实现了Format方法,我们就可以通过fmt的+v
格式化输出堆栈消息。
使用示例:
func data() error {
return errors.New("db error")
}
func main() {
err := data()
fmt.Printf("%v\n", err)
fmt.Printf("%+v\n", err)
}
// 输出如下:
// db error
// db error
// main.data
// f:/GoCode/GoTest/err/main.go:10
// main.main
// f:/GoCode/GoTest/err/main.go:13
// runtime.main
// D:/SoftWare/Go1.18/src/runtime/proc.go:250
// runtime.goexit
// D:/SoftWare/Go1.18/src/runtime/asm_amd64.s:1571
- error包装使用errors.Wrap
对于一个现成的error,需要对它进行再次包装时,有三个方法可以选择:
//只附加新的信息
func WithMessage(err error, message string) error
//只附加调用堆栈信息
func WithStack(err error) error
//同时附加堆栈和信息
func Wrap(err error, message string) error
大概看下它的源码如下;
type withStack struct {
error
*stack
}
type withMessage struct {
cause error
msg string
}
func WithMessage(err error, message string) error {
if err == nil {
return nil
}
return &withMessage{
cause: err,
msg: message,
}
}
func WithStack(err error) error {
if err == nil {
return nil
}
return &withStack{
err,
callers(),
}
}
func Wrap(err error, message string) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: message,
}
return &withStack{
err,
callers(),
}
}
- error的Unwrap使用errors.Cause
这里的Cause方法,就类似Go1.13中的Unwrap方法,会递归获取error的类型,如果嵌套的error实现了Cause方法,则继续获取其Cause方法,直到不是Cause时返回error。
使用Cause可以让我们获得最根本的错误原因。
其源码如下:
func Cause(err error) error {
type causer interface {
Cause() error
}
for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}
从源码可以看出来,就是递归的判断是否实现了cause接口
,然后在源码里我们可以看到,定义的每个类型都是实现了Cause接口的,返回的都是自身的error:
type withStack struct {
error
*stack
}
func (w *withStack) Cause() error { return w.error }
type withMessage struct {
cause error
msg string
}
func (w *withMessage) Cause() error { return w.cause }
所以在使用pkg/errors提供的error包装方法时,都是实现了Cause方法的,在调用errors.Cause时,就能递归的查找到最终的根因。
使用示例:
func data() error {
return errors.New("db error")
}
func biz() error {
if err := data(); err != nil {
return errors.Wrap(err, "biz error")
}
return nil
}
func service() error {
if err := biz(); err != nil {
return errors.Wrap(err, "service error")
}
return nil
}
func main() {
err := service()
fmt.Println(errors.Cause(err)) // 输出:db error
}
为了兼容Go1.13中errors包的新接口,pkg/errors包也做了兼容,提供了几个接口:
package errors
import (
stderrors "errors"
)
func Is(err, target error) bool { return stderrors.Is(err, target) }
func As(err error, target interface{}) bool { return stderrors.As(err, target) }
func Unwrap(err error) error {
return stderrors.Unwrap(err)
}
实质上调用的还是标准库的方法!因此使用pkg/errors包装的error,要解包时还是使用它的Cause方法才找到最开始的根因,因为pkg/errors的New方法返回的对象是没有实现Unwrap方法的。
四.小结
随着Go官方的error发展,目前pkg/errors在github上已经是只读状态;如果需要用到stack信息,则使用pkg/errors,否则推荐使用Go1.13的方法。
对于error的处理有毛剑老师的课程几个建议:
- 在应用程序中出现错误时,使用
errors.New
或者errors.Errorf
返回错误 - 如果是调用应用程序的其他函数出现错误,请直接返回,如果需要携带信息,请使用
errors.WithMessage
- 如果是调用其他库(标准库,第三方库)获取到错误时,请使用
errors.Wrap
添加堆栈信息- 切记,不要每个地方都是用
errors.Wrap
,只需要在错误第一次出现时进行errors.Wrap
即可 - 根据场景进行判断是否需要将其他库的原始错误吞掉,例如可以把
repository
层的数据库相关错误吞掉,返回业务错误码,避免后续我们分割微服务或者更换orm库时需要去修改上层代码 - 注意我们在基础库,被大量引入的第三方库编写时一般不使用errors.Wrap,避免堆栈信息重复
- 切记,不要每个地方都是用
- 禁止每个出错的地方都打日志,只需要在进程最开始的地方使用%+v进行统一打印
我的理解就是:
- 使用堆栈跟踪创建错误,如果是自己的程序可以在最底层用pkg/errors创建带有堆栈信息的错误,如果是调用第三方库,这个库没有使用pkg/errors时,就用errors.Wrap包装带有堆栈信息。
- 因为无法控制第三方库,所以最好的方案是在我们的程序中对第三方库的错误进行包装
- 只有顶级函数才能处理错误,其他函数只将错误往上传播,中间函数不记录,不处理错误,也不丢弃错误,可以添加必要的信息。
- 对于panic,基本不使用,如果调用的第三方库会产生panic,那么我们应该使用defer+recover来避免程序停止,在recover中打印记录log