Go 如何优雅的处理错误

Go Bible

Go 的错误机制

一开始接触 Go 的时候,对多返回值不以为然,在 C 语言中,直接返回一个结构体也可以实现类似多返回值的效果,在 C++就更不用说了。
但是,后面渐渐发现这种多返回值可以额外返回一个实现了error接口的值,可以给我们额外的判断信息。

这样的做法对我而言最大的好处就是,可以更快得上手一个没有见过的函数。
在 Golang 中,一个函数如果没有返回error,往往意味着我们不需要担心函数出错,即不用担心函数会失败。
而在 C 语言中,有些函数会返回NULL代表函数执行失败,比如说malloc,fopen等,
如果需要更具体的信息,我们可以从读取 C 预定义的错误变量来判断时候有错误产生,还可以使用perror来输出详细的错误文本。
所谓,C 更是一个隐式的错误处理,你完全可以直接忽略,但是后果需要自己承担。此外,必须在执行一个函数后立马检查,因为后续的函数可能会覆盖之前的信息。

而在,golang 中,这一切都是显式的,这就要求我们处理那些带有error返回的函数,尽管我处理的做换就是直接返回这个error或者更进一步,给error添加一点额外的信息后在返回。于是我们可以在 golang 随处见到如下代码

if err != nil {
    return err
}

一行代码一个错误

Go 常见的错误处理方法

下面我想说一说平时我是怎么处理error

  • 让它崩溃吧
if err != nil {
    log.Panic(err)
}
  • 直接返回
  • 添加一些附加信息
if err != nil {
    return fmt.Errorf("Error: %v",err)
}

这上面可能是我比较容易想到的,上面的处理很多都是不负责任,仅仅是把错误向上抛,而往往最终的结果也就是在log里面打印一条出错信息。
但是,这样在调试的时候非常不友好,有可能一个函数在很多个地方调用,最后输出的错误信息是一样的,非常不方便调试。
此外,一些err是带有附加结构体信息的,如果使用fmt.Errorf方式的话会丢失这些信息,此外,一些错误可能是莫一个哨兵方式存在,例如err == io.EOF这样的处理就不行了。

Go errors 库的演化

go1.13之前,errors库是非常简洁的,大概如下

package errors

func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

没错,就是怎么的简洁,仅仅是一个实现了error接口的结构体。但是,功能确实很实用。
不过,这样的缺少了许多的信息,我们虽然可以在一个string里面包含更多的错误信息,
但是这样仅仅是对于human而言。为此,更好的做法是,我们自定义自己的错误类型。

type MyError struct {
    caller string //调用函数
    typ string //错误类型
    msg string //错误消息
}
func (m myError) Error() string {
    return fmt.Sprintf("%s: %s from %s",m.typ,m.msg,m.caller)
}
func IsMyError(err error) bool {
    _,ok := err.(*MyError)
    return ok
}
  • 使用类型断言来识别错误
err := SomeFunc()
switch err {
    case nil:
        //nothing wrong
    case *MyError:
        //MyError handler
    default:
        //other error type
}

//或者
func MyFunc(){
    defer func(){
        err := recover()
        switch err := err.(type) {
            case nil:
                return
            case *myError:
                fmt.Printf("%v",err)
            default:
                panic(err)
        }
    }
    panic(&myError{
        typ: "MyErrorType",
        msg: "this is a test message",
        caller: "MyFunc",
    })
}

但是这样带来的另一个坏处就是,我们的结构体必须是要导出的,这样用户才能够使用。
如果有多个不同的结构体,就会导致这种形式的滥用。不过这种形式可以在包内自己使用,而不是暴露出来。

  • 假设错误的行为,而不是类型(Assert errors for behaviour, not type)

在 golang 中,interface就是莫一类行为的抽象,单反满足这类行为的结构体就实现了这个接口。
在 golang 中,常用的行为(接口)有io.Writer,io.Reader

假定错误的行为,这个是 Dave 最推崇的一种方式,名为Opaque Errors
但是,我不是很理解这一个理念,或者说我根本没有使用过这样的方式。

//我们假设错误实现了某一种行为(接口)
type temporary interface{
    Temporary() bool
}
func IsTemporary(err error) bool {
    te,ok := err.(temporary)
    return ok && te.Temporary()
}

我觉得上面的方法写起来挺麻烦的,我更在乎error包含了那些信息,而不是error所具有那些行为。
虽然,上面的抽象很高,但是目前我还是用不来。


其实上面说了怎么多,我们无非是要在error上面传递更多的上下文,同时避免一大串的类型断言。
其实,已经有写好的包了github.com/pkg/errors
此外,golang 在1.13版本已经拓展了errors
我们可以使用这些包,更加方便的携带上下文。

使用 pkg/errors

  • 链接
  • wrapper error(adding context to an erro)
jsonBytes, err := json.Marshal(tempMsg)
if err != nil {
    return errors.Wrap(err, "marshal")
}
// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
    if err == nil {
        return nil
    }
    err = &withMessage{
        cause: err,
        msg:   message,
    }
    return &withStack{
        err,
        callers(),
    }
}

这里我们看到Wrap的实现携带了stack信息,这样可以方便我们调试。

  • retrieve the cause of an error
type causer interface{
    Cause() error
}
//errors.Cause会递归解包直到没有实现`causer`接口的结构体
switch err := errors.Cause(err).(type) {
    case *MyError:
        //handle you own error type
    default:
        //do default
}

Golang 1.13 后拓展的 errors

errors引入了三个新函数

  • func Unwrap(err error) error
  • func Is(err, target error) bool
  • func As(err error, target interface{}) bool

没有wrap函数?
其实在fmt.Frintf("%w",err)里面
下面是使用的 demo

type myError struct {
    typ    string
    msg    string
    caller string
}
func (m myError) Error() string {
    return fmt.Sprintf("%s: %s from %s", m.typ, m.msg, m.caller)
}
func TestMyError() {
    defer func() {
        err := recover()
        switch err := err.(type) {
        case nil:
            return
        case *myError:
            fmt.Printf("MyError: %v\n\n", err)
        default:
            panic(err)
        }
    }()
    panic(&myError{
        typ:    "MyErrorType",
        msg:    "This is a test message",
        caller: "TestMyError",
    })
}
func main() {
    TestMyError()

    originErr := &myError{
        typ:    "myerror",
        msg:    "this is origin error",
        caller: "main",
    }

    fmt.Printf("%#v\n\n", originErr)
    wrapErr := fmt.Errorf("Wrap the original error: %w", originErr)
    fmt.Printf("%#v\n\n", wrapErr)

    if errors.Is(wrapErr, originErr) {
        fmt.Printf("it is origin error\n")
    } else {
        fmt.Printf("it is not the origin error\n")
    }

    err := errors.Unwrap(wrapErr)
    fmt.Printf("unwrap: %v\n", err)

    targetErr := &myError{}
    if errors.As(wrapErr, &targetErr) {
        fmt.Printf("target: %v", targetErr)
    }
}

go 标准库为我们提供了对error进行Wrap的操作,但还是需要我们自己来传递额外的上下文。

总结

这里的error处理绝对不是 golang 的error的终点

我相信 go2,会对这情况进行改进,
虽然 go2 的草案提供的error处理方案非常的ugly
但是我还是很期待go2的表现

Last modification:April 17th, 2020 at 02:32 pm
要饭啦~