Go错误处理的一些总结

2,376 阅读4分钟

Golang有很多优点,但是Go对错误处理的支持目前并不理想,以至于一直有一个if err != nil的梗流传于Gopher间。即便如此Gopher们也在不断的努力探索着各种优雅的解决方案。笔者就目前一些常用的解决方案进行了总结整理。

函数内错误处理

  • 最简单粗暴的办法

通过return方式直接传给调用者进行处理。如:

if err := Foo(); err != nil {
    return err
}

然而,在实际使用中我们往往需要将包含一些自定义的错误信息,这是容易形成每一个err都需要判断,并且转载对应错误信息的情况。如:

// 这里传递错误信息下方法也有很多种,后续内容会提到。这里仅使用New方法举例。
if err := Foo(); err != nil {
    return errors.New("failed to ...")
}
​
if err := Bar(); err != nil {
    return errors.New("failed to ...")
}
​
...

这时候我们就会希望能有一个集中处理错误的地方,也就自然而然的产生了下面这几种方法。

  • return + defer 方式

遇到错误直接return,利用defer在函数结束后统一处理。

func SomeFunc() {
    var err error
    defer func() {
        if err != nil {
            switch err {
            case io.EOF:
                fmt.Println(err)
            case sql.ErrConnDone:
                fmt.Println(err)
            default:
                fmt.Println("unknown err")
            }
            return
        }
​
        fmt.Println("I'm ok!")
    }()
​
    if err = foo(); err != nil {
        return
    }
    if err = bar(); err != nil {
        return
    }
}
  • goto + Lable方式

    使用ERR在代码最后标记一个错误处理代码区,当发生错误时候,使用goto跳转到错误处理区,进行错误的集中处理。而如果代码正常运行,会直接通过return结束,并不会进入错误处理区。

func SomeFunc() {
    var err error
    if err = foo(); err != nil {
        goto ERR
    }
​
    if err = bar(); err != nil {
        goto ERR
    }
​
    return
ERR:
    switch err {
        case io.EOF:
        fmt.Println(err.Error())
        case sql.ErrConnDone:
        fmt.Println(err.Error())
    }
 
}
​
  • panic + recover 不推荐

遇到错误直接panic,使用recover捕获

func Run() {
    defer func() {
        if err := recover(); err != nil {
            // 错误处理
        }
    }()
    if err := foo(); err != nil {
        panic(err)
    }
}
​

不推荐的原因:

  1. 混淆了错误和异常的概念。
  2. 开销远大于之前两种方式。
  3. 风险大,稍有不甚就会导致整个程序崩溃。

虽说不推荐,但还是把它拿出来讲,是因为这个方式可以不用每个函数都搞一个错误处理区,而是可以在顶层调用者处进行recover,统一处理。

注意: 携程之间的panic是不能通过recover捕捉到的。一个携程因为panic崩溃会导致整个程序崩溃。

func foo() {
    panic("foo failed")
}
​
func Bar() {
    panic("bar failed")
}
​
func SomeFunc() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()
    foo()
    bar()
}

错误信息的携带

主要由两个方向:一个是预定义错误;另一个是自定义错误类。

  • 预定义错误

io包中的一些预定义错误

// ErrShortWrite means that a write accepted fewer bytes than requested
// but failed to return an explicit error.
var ErrShortWrite = errors.New("short write")
​
// errInvalidWrite means that a write returned an impossible count.
var errInvalidWrite = errors.New("invalid write result")
​
// ErrShortBuffer means that a read required a longer buffer than was provided.
var ErrShortBuffer = errors.New("short buffer")
​
// EOF is the error returned by Read when no more input is available.
// (Read must return EOF itself, not an error wrapping EOF,
// because callers will test for EOF using ==.)
// Functions should return EOF only to signal a graceful end of input.
// If the EOF occurs unexpectedly in a structured data stream,
// the appropriate error is either ErrUnexpectedEOF or some other error
// giving more detail.
var EOF = errors.New("EOF")
​
...
  • 自定义错误类

    这是最常见的方式。将需要的信息添加到结构体中即可。

type Err struct {
    Err error
    Msg string
    ...
}

甚至还可以通过实现error接口,利用官方的error进行传递。

// 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
}

实践方案:

注意: 自定义错误类方式往往会存在错误嵌套,我们要尽量减少不必要的包裹。以免造成错误链过长,影响性能。

  • Wrap方案

这是Go1.13引入的错误处理方式,据说源自于golang.org/x/xerrors

Go1.13引入了新的格式化动词: %w,用于实现Wrap效果。

fmt.Errorf("failed to login: %w", err)
// 错误处理可以使用以下方法// AS: 按顺序寻找错误链中是否有与目标匹配的错误? 
// 第二个参数可以是任何实现了error接口的非空类型
// 如果有返回true,并将err替换成错误链上第一个匹配上的
// 否则返回false
func As(err error, target any) bool// Is: 判断错误链上是否有能匹配上的错误
func Is(err, target error) bool// Unwrap 解开一层错误链
func Unwrap(err error) error

一点不足:无法直接答应调用栈信息,并且Go团队也没有明确的计划。

  • github.com/pkg/errors

    1. Wrap 方法用来包装底层错误,增加上下文文本信息并附加调用栈。 一般用于包装对第三方代码(标准库或第三方库)的调用。
    1. WithMessage 方法仅增加上下文文本信息,不附加调用栈。 如果确定错误已被 Wrap 过或不关心调用栈,可以使用此方法。 注意:不要反复 Wrap ,会导致调用栈重复
    1. Cause方法用来判断底层错误 。
func foo() error {
   return errors.Wrap(sql.ErrNoRows, "foo failed")
}
​
func bar() error {
   return errors.WithMessage(foo(), "bar failed")
}
​
func main() {
   err := bar()
   if errors.Cause(err) == sql.ErrNoRows {
      fmt.Printf("data not found, %v\n", err) // 不会打印调用栈信息
      fmt.Printf("%+v\n", err) // 会输出调用栈信息
      return
   }
   if err != nil {
      // unknown error
   }
}

\