Go错误处理实践

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设计,从上面的使用可以感受到有以下几个弊端:

  1. error会大量穿插在代码中,基本返回值有error的,都需要进行一次if err != nil判断,整个代码下来占据大量,可读性比较差
  2. 很多重复的if err != nil片段,没法简化
  3. 简单的使用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
  1. 原始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
  1. 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(),
	}
}
  1. 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的处理有毛剑老师的课程几个建议:

  1. 在应用程序中出现错误时,使用errors.New或者errors.Errorf返回错误
  2. 如果是调用应用程序的其他函数出现错误,请直接返回,如果需要携带信息,请使用errors.WithMessage
  3. 如果是调用其他库(标准库,第三方库)获取到错误时,请使用errors.Wrap添加堆栈信息
    1. 切记,不要每个地方都是用errors.Wrap,只需要在错误第一次出现时进行errors.Wrap即可
    2. 根据场景进行判断是否需要将其他库的原始错误吞掉,例如可以把repository层的数据库相关错误吞掉,返回业务错误码,避免后续我们分割微服务或者更换orm库时需要去修改上层代码
    3. 注意我们在基础库,被大量引入的第三方库编写时一般不使用errors.Wrap,避免堆栈信息重复
  4. 禁止每个出错的地方都打日志,只需要在进程最开始的地方使用%+v进行统一打印

我的理解就是:

  1. 使用堆栈跟踪创建错误,如果是自己的程序可以在最底层用pkg/errors创建带有堆栈信息的错误,如果是调用第三方库,这个库没有使用pkg/errors时,就用errors.Wrap包装带有堆栈信息。
  2. 因为无法控制第三方库,所以最好的方案是在我们的程序中对第三方库的错误进行包装
  3. 只有顶级函数才能处理错误,其他函数只将错误往上传播,中间函数不记录,不处理错误,也不丢弃错误,可以添加必要的信息。
  4. 对于panic,基本不使用,如果调用的第三方库会产生panic,那么我们应该使用defer+recover来避免程序停止,在recover中打印记录log

五.学习资料

  1. Dave cheney GoCon 2016 演讲
  2. Tony Bai Go语言错误处理
  3. 如何优雅的在Golang中进行错误处理
  4. Go语言(golang)的错误(error)处理的推荐方案
  5. Go语言(golang)新发布的1.13中的Error Wrapping深度分析
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值