暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

golang 错误处理

老码农空杯修行记 2021-05-15
914

1、errors 包

  • 底层实现

    // 工厂方法,创建错误
    // 可以推断 error 是一个接口:
    //
    // type error interface {
    // Error() string
    // }
    //
    func New(text string) error {
      return errorString{text}
    }


    // errorString 结构体实现了 error 接口
    type errorString struct {
    s string
    }


    // 该结构体只有一个方法,返回错误字符串
    func (e *errorString) Error() string {
    return e.s
    }
    • error 包的缺陷

    • 缺陷一:无调用栈信息

      var (
      AssertError = errors.New("assert error")
      )


      // 直接返回错误
      func f() error {
      return AssertError
      }


      // c() 调用 f()
      func c() error {
        // golang 一般错误处理方式
      err := f()
      if err != nil {
      return err
      }


      fmt.Println("do somthing")
      return nil
      }


      func main() {
      err := c()
      if err != nil {
      fmt.Printf("main invoke c(): %+v\n", err)
      }
      }
      执行完输出
        main invoke c(): assert error
        从输出仅仅看到最后打印出错地方,不能直观的看到错误来自哪里,需要看代码一步一步定位,上面比较简单,基本一下就能定位来自 f(), 但是若是 c() 里面调用了f() 多次,此时判断来自哪里就需要额外的日志或打印了,当然可以在错误之上增加额外的上下文信息,但是会引入下面的第二个缺陷
        • 缺陷二:添加上下文信息错误发生变化

          func f() error {
          return AssertError
          }


          func c() error {


          err := f()
          if err != nil {
              // 错误中添加上下文信息
          return fmt.Errorf("c invoke f err, %v", err)
          }


          fmt.Println("do somthing")
          return nil
          }


          func main() {
          err := c()


            // 如果是 AssertError,则一种处理方式
            // 其它错误走额外的分支
          if err == AssertError {
          fmt.Println("main invoke c(): AssertError\n")
          } else {
          fmt.Printf("unknown err: %+v\n", err)
          }
          }
          运行输出
            unknown err: c invoke f err, assert error
            现在也能定位到问题出现的地方,但是添加上下文之后类型值发生了变化,main 函数判断时发现不是 AssertError,所以走了 else 分支,但此处本质上代码就是 AssertError 产生的。

            2、开源错误处理推荐

            github.com/pkg/errors 在原来 go 内置 error 基础上增加了栈信息和上下文信息,很好的弥补了上面内置 error 的缺陷。

              func f() error {
              // Wrap 在 error 基础上添加调用栈信息,同时添加额外的
              // 用户上下文信息,如果err是nil,则返回的是nil
              return pkgerr.Wrap(AssertError, "inner f err")


              // WithStack 只添加调用栈信息
              //return pkgerr.WithStack(AssertError)
              }


              func c() error {


              err := f()
              if err != nil {
              // WithMessage 仅仅添加上下文信息
              return pkgerr.WithMessage(err, "c invoke f")
              }


              fmt.Println("do somthing")
              return nil
              }


              func main() {
              err := c()
              if err != AssertError {
              fmt.Printf("main invoke c()--------------------->: %+v\n", err)
              }
              }
              输出
                main invoke c()--------------------->: assert error
                inner f err
                main.f
                Users/dkos/t/newquick.go:17
                main.c
                Users/dkos/t/newquick.go:25
                main.main
                Users/dkos/t/newquick.go:36
                runtime.main
                usr/local/go/src/runtime/proc.go:225
                runtime.goexit
                usr/local/go/src/runtime/asm_amd64.s:1371
                c invoke f

                上面输出了完整的调用栈,同时错误类型没有改变。我们再看看主要的几个函数几个函数是如何实现的:

                • Wrap包装一个错误,添加上下文和栈信息

                  func Wrap(err error, message string) error {
                    // 本身没有错误的话,直接返回 nil
                  if err == nil {
                  return nil
                  }
                    
                    // withMessage 实现了 error 接口
                    // 同时提供了其它功能
                    //
                    // cause: 存储错误
                    // msg: 用户要添加的上下文信息
                    err = &withMessage{
                  cause: err,
                  msg: message,
                  }

                  // withStack 继承了(嵌入了) error,
                  // 同时调用底层 runtime.Callers 读取
                    // 调用栈信息
                  return &withStack{
                  err,
                  callers(),
                  }
                  }
                  • WithMessage:Wrap 的简化版本,值添加上下文信息
                    func WithMessage(err error, message string) error {
                    // 本身没有错误的话,直接返回 nil
                    if err == nil {
                    return nil
                    }

                      // 这里如果 err 被 wrap 的话,那么本身就含有栈信息
                      // 若是 err 没有 Wrap 过,直接添加上下文信息返回
                    return &withMessage{
                    cause: err,
                    msg: message,
                    }
                    }
                    • Cause获取触发错误的原始错误

                      func Cause(err error) error {
                      type causer interface {
                      Cause() error
                      }


                      for err != nil {
                          // 判断 err 有无实现 cause 接口
                          // 如果本身没有实现,则 err 本身
                          // 就是触发错误的原始错误
                      cause, ok := err.(causer)
                      if !ok {
                      break
                      }
                      // 如果 err 实现了 cause 接口,
                      // 那么说明它还有底层错误,此时
                          // 循环查询错误链,直到查到没有
                          // 实现 causer 接口的错误,该
                          // 错误就是为经过加工的最原始错误
                      err = cause.Cause()
                      }

                      // 返回原始错误
                      return err
                      }
                      • Is判断目标错误是否在原始错误链上

                        func Is(err, target error) bool {
                          // 可以判断错误是否为 nil
                        if target == nil {
                        return err == target
                        }


                          // 检查 error 是否为可比类型
                        isComparable := reflectlite.TypeOf(target).Comparable()
                        for {
                            // 可比时,判定是否w诶目标,是则直接返回
                        if isComparable && err == target {
                        return true
                        }

                            // 判断是否实现 Is 接口,若实现了,则直接可以用
                            // Is 接口判定,但是对于这里的 github.com/pkg/errors
                            // 提供的几个结构都没有实现该接口
                        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
                        return true
                        }

                        // 若是没有实现 Is 接口,则调用 Unwrap,它会调用
                            // 实际实现 error 接口的类型的 Unwrap 拿到底层错误
                            // 实际取的就是 cause 存储的 error,Unrap 也是一个
                            // 拆错误链的过程,边拆边比较看是否和目标相同
                        if err = Unwrap(err); err == nil {
                        return false
                        }
                        }
                        }
                        3、总结
                        由于自带 error 包存在缺失栈信息和无法添加上下文的条件,因此推荐使用开源的 github.com/pkg/errors 包,它弥补了自带 error 的缺陷。
                        文章转载自老码农空杯修行记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                        评论