介绍
golang 程序大多数是通过 if err != nil
处理错误,在 golang 社区中,有一部分 golang 程序员对此举是持反对观点,他们认为在 golang 代码中存在大量的错误处理代码 if err != nil
,使整体代码变得非常不优雅,应该在 golang 中引入其他处理错误的机制,例如 try-catche
或其它此类处理错误的机制。其实,他们忽略了 golang 中一个特别重要的概念,即 errors are values
,并且 golang 作者 Rob Pike 也对此问题做出过回应,在 golang 代码中出现重复的错误处理代码 if err != nil
,可能是 golang 用户的使用方式有问题。
本文我们主要聊聊在 golang 中,怎么处理错误?
golang 定义错误的两种方式
使用 golang 标准库 errors 的 New() 函数,可以定义一个错误类型的变量。
func New(text string) error
New() 函数接收一个 string 类型的文本,返回一个 error 类型的变量。即使给定的文本不同,每次对 New() 函数的调用也会返回不同的错误值。
关于每次调用 New() 函数,都可以返回不同的错误值,golang 是怎么做到的呢?我们通过阅读 golang 的源码,找一下我们的问题答案。
源码 /usr/local/go/src/errors/errors.go
:
// 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}
,所以我们的疑问自然有了答案。
那么,golang 中定义错误的另外一种方式是什么?在 golang 标准库 fmt 中,通过调用 Errorf() 函数也可以返回一个 error 类型的错误。
源码 /usr/local/go/src/fmt/errors.go
:
func Errorf(format string, a ...interface{}) 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 }
03
错误处理方式之“不透明错误处理”
正如我们在文章开篇所述,在 golang 程序中,我们见的最多的错误处理方式就是 if err != nil
,此种错误处理方式,错误处理方不关心错误提供方的错误值。因此,我们将此种错误处理方式称为“不透明错误处理”。
示例代码:
err := errors.New("this is a error example") if err != nil { fmt.Println(err) return }
04
golang 1.13 新增 As()
函数
在 golang 1.13 中,新增 As()
函数,当 error 类型的变量是一个包装错误(wrap error)时,它可以顺着错误链(error chain)上所有被包装的错误(wrapped error)的类型做比较,直到找到一个匹配的错误类型,并返回 true,如果找不到,则返回 false。
通常,我们会使用 As()
函数判断一个 error 类型的变量是否为特定的自定义错误类型。
示例代码:
// 自定义的错误类型 type DefineError struct { msg string } func (d *DefineError) Error() string { return d.msg } func main() { // wrap error err1 := &DefineError{"this is a define error type"} err2 := fmt.Errorf("wrap err2: %w\n", err1) err3 := fmt.Errorf("wrap err3: %w\n", err2) var err4 *DefineError if errors.As(err3, &err4) { // errors.As() 顺着错误链,从 err3 一直找到被包装最底层的错误值 err1,并且将 err3 与其自定义类型 `var err4 *DefineError` 匹配成功。 fmt.Println("err1 is a variable of the DefineError type") fmt.Println(err4 == err1) return } fmt.Println("err1 is not a variable of the DefineError type") }
05
golang 1.13 新增 Is()
函数
在 Part03 中,我们讲述了“不透明错误处理”的错误处理方式,错误处理方不关心错误提供方的错误值。但是,在错误处理方需要关心错误提供方的错误值时,错误处理方要对错误提供方的错误值进行判定,这就造成了代码的耦合,错误提供方的错误值每次修改,错误处理方都需要跟着做出相应修改。
针对这种情况,golang 一般会采用“哨兵错误处理”的错误处理方式,即定义可导出的错误变量,错误处理方和错误提供方都只操作错误变量,这样做的好处是只需维护错误变量,但是还没有彻底解决问题,如果 error 类型的错误变量是一个包装错误(wrap error),“哨兵错误处理”的错误处理方式也不方便处理该错误。
好在 golang 1.13 新增 Is()
函数,它可以顺着错误链(error chain)上所有被包装的错误(wrapped error)的类型做比较,直到找到一个匹配的错误。
示例代码:
// 哨兵错误处理 var ( ErrInvalidUser = errors.New("invalid user") ErrNotFoundUser = errors.New("not found user") ) func main () { err1 := fmt.Errorf("wrap err1: %w\n", ErrInvalidUser) err2 := fmt.Errorf("wrap err2: %w\n", err1) // golang 1.13 新增 Is() 函数 if errors.Is(err2, ErrInvalidUser) { fmt.Println(ErrInvalidUser) return } fmt.Println("success") }
06
总结
本文我们开篇先是讲述了 golang 社区中,存在对待 golang 错误处理方式的反对态度的用户,这么一个客观事实。接着,我们介绍了 golang 中的两种定义错误的方式和底层源码实现,和 golang 1.13 中新增的关于错误处理的函数。如果你现在使用的是 golang 1.13 及以上版本,请使用 As()
和 Is()
。
通过阅读源码 /usr/local/go/src/errors/wrap.go
,我们可以发现 As()
和 Is()
是通过在错误链中不断调用 Unwrap()
函数,最终找到匹配的错误值。其中,Unwrap()
函数也是在 golang 1.13 中新增的函数。
Unwrap()
函数的源码:
// 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() }
推荐阅读:
参考资料:
https://golang.org/pkg/errors/
延伸阅读:
https://blog.golang.org/error-handling-and-go
https://blog.golang.org/errors-are-values
https://blog.golang.org/go1.13-errors
https://golang.org/doc/tutorial/handle-errors
https://medium.com/rungo/error-handling-in-go-f0125de052f0
https://www.digitalocean.com/community/tutorials/handling-errors-in-go