大家好,我是 frank。
欢迎大家点击上方蓝色文字「Golang 语言开发栈」关注公众号。
01
介绍
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 中,怎么处理错误?
02
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()
}
推荐阅读:
Golang 语言三方库 lumberjack 日志切割组件怎么使用?
参考资料:
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
扫描二维码,加入微信群
感谢支持,请点「赞」和「在看」⬇️
⬇️更多精彩内容,请点击「阅读原文」
文章评论