之前写过几篇关于 Go 错误处理的文章,发现文章里不少知识点都有点落伍了,比如Go在1.13后对错误处理增加了一些支持,最大的变化就是支持了错误包装(Error Wrapping),以前想要在调用链路的函数里包装错误都是用"github.com/pkg/errors"
这个库。
Go 在2019年发布的Go1.13
版本也采纳了错误包装,并且还提供了几个很有用的工具函数让我们能更好地使用包装错误。这篇文章就来主要说一下这方面的知识点,不过开始我们还是再次强调一下使用 Go Error 的误区,避免我们从其他语言切换过来时给自己后面挖坑。
自定义错误要实现error接口
这一条估计很多人都知道,但是文章开头开始先从这个惯例开始,因为我以前待过一个PHP转Go的研发团队,可能大家一开始都不太会,才有了这种错误的使用方式。
首先我们再复述一遍,Go
通过error
类型的值表示程序里的错误。
error
类型是一个内建接口类型,该接口只规定了一个返回字符串值的Error
方法。
go
type error interface {
Error() string
}
Go
程序的函数经常会返回一个error
值
go
package strconv
func Atoi(s string) (int, error) {
....
}
调用者通过测试error
值是否是nil
来进行错误处理。
go
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)
error
为nil
时表示成功;非nil
的error
表示失败。
说完 Go
里 error
最基本的使用方式后,接下来说项目里的自定义错误类型。假如项目在 Dao 层定义了一个这样的错误类型来记录数据库查询错误。
go
type MyError struct {
Sql string
Param string
Err error
}
假如,这个自定义的MyError
不去实现error
接口,Dao 层里的函数返回的都是MyError
的话。
go
func FindUserRowByPhoneMyError(userId int) (user User, MyError error) {
......
}
那么使用这些 Dao 函数的代码逻辑层都得引入dao.MyError
这个额外的类型。有人会说,我把MyError
定义在公共包里,所有代码逻辑层、Dao 层都用这个common.MyError
总没啥问题了吧。
使用上乍一看没什么问题,但其实最大的问题就是不兼容、不符合Go语言对错误的接口约束,就没法对自定义错误类型使用Go对error
提供的其他功能了,比如说后面要介绍的错误包装。
所以针对自定义的错误类型,我们也要让他变成一个真正的Go error,方法就是让它实现error
接口定义的方法。
go
func (e *MyError) Error() string {
return fmt.Sprintf("sql: %s, params: %s, err: %s", e.Sql, e.Param, e.Err.Error())
}
包装错误
在现实的程序应用里,一个逻辑往往要经多多层函数的调用才能完成,那在程序里我们的建议Error Handling 尽量留给上层的调用函数做,中间和底层的函数通过错误包装把自己要记的错误信息附加再原始错误上再返回给外层函数。
比如像下面这样
go
func doAnotherThing() error {
return errors.New("error doing another thing")
}
func doSomething() error {
err := doAnotherThing()
return fmt.Errorf("error doing something: %v", err)
}
func main() {
err := doSomething()
fmt.Println(err)
}
这段代码从打印错误信息的输出上看没什么问题,但是深层次的问题很明显,我们丢失了原来的err
,因为它已经被我们的fmt.Errorf
函数转成一个新的字符串了。
基于这个背景,很多开源三方库提供了错误包装、追加错误调用栈等功能,用的最多的就是"github.com/pkg/errors"
这个库,提供了下面几个主要的包装错误的功能。
go
//只附加新的信息
func WithMessage(err error, message string) error
//只附加调用堆栈信息
func WithStack(err error) error
//同时附加堆栈和信息
func Wrap(err error, message string) error
Go官方在2019年发布1.13
版本,自己也增加了对错误包装的支持,不过并没有提供什么Wrap
函数,而是扩展了fmt.Errorf
函数,加了一个%w
来生成一个包装错误。
css
e := errors.New("原始错误")
w := fmt.Errorf("外面包了一个错误%w", e)
Go1.13
引入了包装错误后,同时为内置的errors
包添加了3个函数,分别是Unwrap
、Is
和As
。
先来聊聊Unwrap
,顾名思义,它的功能就是为了获取到包装错误里那个被嵌套的error。
go
func Unwrap(err error) error {
//先判断是否是wrapping error
u, ok := err.(interface {
Unwrap() error
})
//如果不是,返回nil
if !ok {
return nil
}
//否则则调用该error的Unwrap方法返回被嵌套的error
return u.Unwrap()
}
这里需要注意的是,嵌套可以有很多层,我们调用一次errors.Unwrap
函数只能返回往里一层的error
,如果想获取更里面的,需要调用多次errors.Unwrap
函数。最终如果一个error
不是warpping error,那么返回的是nil
。
如果想得到最原始的error
,建议自己封装个工具函数,类似这样
go
func Cause(err error) error {
for err != nil {
err = errors.Unwrap(err)
}
return err
}
对于我们文章开头定义的那个自定义错误MyError
想要把它变成可包装的Error的话,还需要实现一个Unwrap()
方法。
go
func (e *MyError) Unwrap() error { return e.Err }
有了包装错误后,像具体某种错误的判断和错误的类型转换也得需要跟进改一下才行。这就是errors
包在1.13
后新增的另外两个工具函数Is
和As
的作用。接下来我们一个个来说。
errors.Is
在Go 1.13之前没有包装错误的时候,程序里要判断是不是同一个error可以直接简单粗暴的:
ini
if err == os.ErrNotExists {
......
}
这样我们就可以通过判断来做一些事情。但是现在有了包装错误后这样办法就不完美的,因为你根本不知道返回的这个err
是不是一个嵌套的error,嵌套了几层。所以基于这种情况,Go为我们提供了errors.Is
函数。
go
func Is(err, target error) bool
- 如果
err
和目标错误target
是同一个,那么返回true
。 - 如果
err
是一个包装错误,目标错误target
也包含在这个嵌套错误链中的话,那么也返回true
。
下面是一个使用errors.Is
判断是否是同一错误的例子。
go
var ErrDivideByZero = errors.New("divide by zero")
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, ErrDivideByZero
}
return a / b, nil
}
func main() {
a, b := 10, 0
result, err := Divide(a, b)
if err != nil {
switch {
case errors.Is(err, ErrDivideByZero):
fmt.Println("divide zero error")
default:
fmt.Printf("unexpected division error: %+v\n", err)
}
return
}
fmt.Printf("%d / %d = %d\n", a, b, result)
}
errors.As
同样在没有包装错误前,我们要把error 转换为一个具体类型的error,一般都是使用类型断言或者 type switch,其实也就是类型断言。
css
if pathErr, ok := err.(*os.PathError); ok {
fmt.Println(pathErr.Path)
}
但是有了包装错误之后,返回的err可能是已经被嵌套了,这种方式就不能用了,所以Go为我们在errors
包里提供了As
函数。
go
func As(err error, target interface{}) bool
As
函数所做的就是遍历错误的嵌套链,从里面找到类型符合的error,然后把这个error赋给target参数,这样我们在程序里就可以使用转换后的target了,因为这里有夫之,所以target必须是一个指针,这个也算是Go内置包里的一个惯例了,像json.Unmarshal
也是这样。
所以把上面的例子用As
函数实现就变成了酱婶:
scss
var pathErr *os.PathError
if errors.As(err, pathErr) {
fmt.Println(pathErr.Path)
}
总结
这篇文章主要是更新一下Error处理在Go 1.13以后新增的功能点,以前的文章介绍的更多的还是使用"pkg/errors"那个包的方式,主要是前两年以前公司用的Go版本一直是1.12,所以这部分知识我一直没更新过来,这里简单做个梳理。
Go错误处理以前也写过几篇,建议大家一起看看,有搭建项目的需求时统一看一下。