写点什么

Go error 的四种处理方式

作者:Rayjun
  • 2021 年 12 月 05 日
  • 本文字数:2167 字

    阅读完需:约 7 分钟

Go error 的四种处理方式

Go 的 error 处理一直是被人诟病的地方,但换个角度其实 Go 的 error 还是挺好用的,error 有两个很重要的特性:


  • error 就是一个普通的值,处理起来没有额外的开销

  • error 的扩展性很不错,可以按照不同的场景来自定义错误


而且在 Go1.13 之后,在 errors 包中提供了一些函数,让错误的处理和追踪更加方便一些。这篇文章会结合 errors 中函数,来讨论一下 Go 中常见的 error 使用方式。


需要注意,这里说的 errors 包是指 Go 中的原生 errors 包,而不是 https://github.com/pkg/errors,后者是对原生 errors 的一些封装,使用起来会更加方便。

1. 原生 error

在 Go 的错误处理中,下面的代码占绝大多数:


if err != nil {   //....   return err}
复制代码


在满足业务需求的情况下,这种错误处理其实是最推荐的方式,这种直接透传的方式让代码之间的耦合度更低。在很多情况下,如果不关心错误中的具体信息,使用这种方式就可以了。

2. 提前定义好 error

原生的 error 在有些情况下使用起来就不是很方便,比如我需要获得具体的错误信息,如果还用上面的方式来使用 error,可能会出现下面的代码:


if err != nil && err.Error() == "invalid param" {    //...}
复制代码


写过代码的都知道上面的代码很不优雅,一方面,使用了魔法值,另外如果错误的信息变化之后,这里的代码逻辑就会出错。


可以通过把错误定义成一个变量:


var (    ErrInvalidParam = errors.New("invalid param"))
复制代码


那么上面的代码就可以变成这样:


if err != nil && err == ErrInvalidParam {   //...}
复制代码


如果一次性需要处理的错误比较多,还可以使用 switch 进行处理:


if err != nil {    switch err {    case ErrInvalidParam:      //..      return    case ErrNetWork:      //...      return    case ErrFileNotExist:      //..      return    default:      //...      return    }  }
复制代码


但是这种方式还不完美,因为 error 在传递的过程中,有可能会被包装,以携带更多的堆栈信息,比如下面这样:


if err != nil {    // 在包装错误的时候,这里格式化错误要使用 %w    return fmt.Errorf("add error info: %+v, origin error: %w", "other info", err)}
复制代码


假设上面被包装的错误是 ErrInvalidParam,那么在调用的地方判断错误,就不能使用下面的代码:


if err != nil && err == ErrInvalidParam {   //...}
复制代码


为了解决这个问题, errors.Is 函数可以判断被包装的 error 中是否有预期的 error:


if errors.Is(err, ErrInvalidParam) {    //..}
复制代码


尽量使用 errors.Is 来替代对 error 的比较。

3. 使用自定义的错误类型

上面的 error 使用方式在某些情况下还是不能满足要求。假如对于上面的无效参数 error,业务方想要知道具体是哪个参数无效,直接定义的错误就无法满足要求。


error 本质是一个接口,也就是是说,只要实现了 Error 方法,就是一个 error 类型:


type error interface {  Error() string}
复制代码


那么就可以自定义一种错误类型:


type ErrInvalidParam struct {    ParamName  string    ParamValue string}
func (e *ErrInvalidParam) Error() string { return fmt.Sprintf("invalid param: %+v, value: %+v", e.ParamName, e.ParamValue)}
复制代码


然后就可以使用类型断言机制或者类型选择机制,来对不同类型的错误进行处理:


e, ok := err.(*ErrInvalidParam)if ok && e != nil {  //...}
复制代码


同样可以在 switch 中使用:


if err != nil {  switch err.(type) {  case *ErrInvalidParam:    //..    return  default:    //...    return  }}
复制代码


在这里 error 同样会存在被包装的问题,而 errors.As 刚好可以用来解决这个问题,可以判断出被包装的错误中是否存在某个 error 类型:


var e *ErrInvalidParamif errors.As(err, &e) {  //..}
复制代码

4. 更灵活的 error 类型

上面的方式已经可以解决大部分场景的 error 处理了,但是在一些复杂的情况下,可能需要从错误中获取更多的信息,还包含一定的逻辑处理。


在 Go 的 net 包中,有这样的一个接口:


type Error interface {    error    Timeout() bool      Temporary() bool}
复制代码


在这个接口中,有两个方法,这两个方法会对这个错误类型进行处理,判断是超时错误还是临时错误,实现了这个接口的 error 要实现这两个 方法,实现具体的判断逻辑。


在处理具体 error 时,会调用相应的方法来判断:


if ne, ok := e.(net.Error); ok && ne.Temporary() {      // 对临时错误进行处理 }
复制代码


if ne, ok := e.(net.Error); ok && ne.Timeout() {      // 对超时错误进行处理 }
复制代码


这种类型的 error 相对来说,使用的会比较少,一般情况下,尽量不要使用这么复杂的处理方式。

5. errors 中的其他能力

在 errors 包中,除了上面提到的 errors.Iserrors.As 两个很有用的函数之外,还有一个比较实用的函数 errors.Unwrap。这个函数可以从包装的错误中将原错误解析出来。


可以使用 fmt.Errorf 来包装 error,需要使用 %w 的格式化:


return fmt.Errorf("add error info: %+v, origin error: %w", "other info", err) 
复制代码


在后续的 error 处理时,可以调用 errors.Unwrap 函数来获得被包装前的 error:


err = errors.Unwrap(err)fmt.Printf("origin error: %+v\n", err)
复制代码


文 / Rayjun


[1] https://go.dev/blog/errors-are-values


[2] https://go.dev/blog/go1.13-errors

发布于: 2 小时前阅读数: 3
用户头像

Rayjun

关注

程序员,王小波死忠粉 2017.10.17 加入

非著名程序员,还在学习如何写代码,公众号同名

评论

发布
暂无评论
Go error 的四种处理方式