目录
  • Golang中的error
  • error的几种玩法
    • 哨兵错误
    • 自定义错误类型
  • Wrap error
    • Golang1.13版本error的新特性
      • errors.UnWrap()
      • errors.Is()
      • errors.As()
    • error处理最佳实践
      • 优先处理error
      • 只处理error一次
      • 不要反复包装error
      • 不透明的错误处理
    • 简化错误处理
      • bufio.scan
      • errWriter
    • 何时该用panic
      • 小补充
        • 总结 

          Golang中的error

          Golang中的 error 就是一个简单的接口类型。只要实现了这个接口,就可以将其视为一种 error

          type error interface {
              Error() string
          }
          

          error的几种玩法

          翻看Golang源码,能看到许多类似于下面的这两种error类型

          哨兵错误

          var EOF = errors.New("EOF")
          var ErrUnexpectedEOF = errors.New("unexpected EOF")
          var ErrNoProgress = errors.New("multiple Read calls return no data or error")
          

          缺点:

          1.让 error 具有二义性

          error != nil不再意味着一定发生了错误
          比如io.Reader返回io.EOF来告知调用者没有更多数据了,然而这又不是一个错误

          2.在两个包之间创建了依赖

          如果你使用了io.EOF来检查是否read完所有的数据,那么代码里一定会导入io包

          自定义错误类型

          一个不错的例子是os.PathError,它的优点是可以附带更多的上下文信息

          type PathError struct {
              Op   string
              Path string
              Err  error
          }
          

          Wrap error

          到这里我们可以发现,Golang 的 error 非常简单,然而简单也意味着有时候是不够用的

          Golang的error一直有两个问题:

          1.error没有附带file:line信息(也就是没有堆栈信息)

          比如这种error,鬼知道代码哪一行报了错,Debug时简直要命

          SERVICE ERROR 2022-03-25T16:32:10.687+0800!!!
                 Error 1406: Data too long for column 'content' at row 1

          2.上层error想附带更多日志信息时,往往会使用fmt.Errorf()fmt.Errorf()会创建一个新的error,底层的error类型就被“吞”掉了

          var errNoRows = errors.New("no rows")
          
          // 模仿sql库返回一个errNoRows
          func sqlExec() error {
              return errNoRows
          }
          
          func serviceNoErrWrap() error {
              err := sqlExec()
              if err != nil {
                  return fmt.Errorf("sqlExec failed.Err:%v", err)
              }
              
              return nil
          }
          
          func TestErrWrap(t *testing.T) {
              // 使用fmt.Errorf创建了一个新的err,丢失了底层err
              err := serviceNoErrWrap()
              if err != errNoRows {
                  log.Println("===== errType don't equal errNoRows =====")
              }
          }
          -------------------------------代码运行结果----------------------------------
          === RUN   TestErrWrap
          2022/03/26 17:19:43 ===== errType don't equal errNoRows =====
          
          

          为了解决这个问题,我们可以使用github.com/pkg/error包,使用errors.withStack()方法将err保
          存到withStack对象

          // withStack结构体保存了error,形成了一条error链。同时*stack字段保存了堆栈信息。
          type withStack struct {
              error
              *stack
          }
          

          也可以使用errors.Wrap(err, "自定义文本"),额外附带一些自定义的文本信息

          源码解读:先将err和message包进withMessage对象,再将withMessage对象和堆栈信息包进withStack对象

          func Wrap(err error, message string) error {
              if err == nil {
                  return nil
              }
              err = &withMessage{
                  cause: err,
                  msg:   message,
              }
              return &withStack{
                  err,
                  callers(),
              }
          }
          

          Golang1.13版本error的新特性

          Golang1.13版本借鉴了github.com/pkg/error包,新增了如下函数,大大增强了 Golang 语言判断 error 类型的能力

          errors.UnWrap()

          // 与errors.Wrap()行为相反
          // 获取err链中的底层err
          func Unwrap(err error) error {
              u, ok := err.(interface {
                  Unwrap() error
              })
              if !ok {
                  return nil
              }
              return u.Unwrap()
          }
          

          errors.Is()

          在1.13版本之前,我们可以用err == targetErr判断err类型
          errors.Is()是其增强版:error 链上的任一err == targetErr,即return true

          // 实践:学习使用errors.Is()
          var errNoRows = errors.New("no rows")
          
          // 模仿sql库返回一个errNoRows
          func sqlExec() error {
              return errNoRows
          }
          
          func service() error {
              err := sqlExec()
              if err != nil {
                  return errors.WithStack(err)    // 包装errNoRows
              }
              
              return nil
          }
          
          func TestErrIs(t *testing.T) {
              err := service()
              
              // errors.Is递归调用errors.UnWrap,命中err链上的任意err即返回true
              if errors.Is(err, errNoRows) {
                  log.Println("===== errors.Is() succeeded =====")
              }
              
              //err经errors.WithStack包装,不能通过 == 判断err类型
              if err == errNoRows {
                  log.Println("err == errNoRows")
              }
          }
          -------------------------------代码运行结果----------------------------------
          === RUN   TestErrIs
          2022/03/25 18:35:00 ===== errors.Is() succeeded =====
          

          例子解读:

          因为使用errors.WithStack包装了sqlErrorsqlError位于error链的底层,上层的error已经不再是sqlError类型,所以使用==无法判断出底层的sqlError

          源码解读:

          • 我们很容易想到其内部调用了err = Unwrap(err)方法来获取error链中底层的error
          • 自定义error类型可以实现Is接口来自定义error类型判断方法
          func Is(err, target error) bool {
              if target == nil {
                  return err == target
              }
              
              isComparable := reflectlite.TypeOf(target).Comparable()
              for {
                  if isComparable && err == target {
                      return true
                  }
                  // 支持自定义error类型判断
                  if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
                      return true
                  }
                  if err = Unwrap(err); err == nil {
                      return false
                  }
              }
          }
          

          下面我们来看看如何自定义error类型判断:

          自定义的errNoRows类型,必须实现Is接口,才能使用erros.Is()进行类型判断

          type errNoRows struct {
              Desc string
          }
          
          func (e errNoRows) Unwrap() error { return e }
          
          func (e errNoRows) Error() string { return e.Desc }
          
          func (e errNoRows) Is(err error) bool {
              return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name()
          }
          
          // 模仿sql库返回一个errNoRows
          func sqlExec() error {
              return &errNoRows{"Kaolengmian NB"}
          }
          
          func service() error {
              err := sqlExec()
              if err != nil {
                  return errors.WithStack(err)
              }
              
              return nil
          }
          
          func serviceNoErrWrap() error {
              err := sqlExec()
              if err != nil {
                  return fmt.Errorf("sqlExec failed.Err:%v", err)
              }
              
              return nil
          }
          
          func TestErrIs(t *testing.T) {
              err := service()
              
              if errors.Is(err, errNoRows{}) {
                  log.Println("===== errors.Is() succeeded =====")
              }
          }
          -------------------------------代码运行结果----------------------------------
          === RUN   TestErrIs
          2022/03/25 18:35:00 ===== errors.Is() succeeded =====
          

          errors.As()

          在1.13版本之前,我们可以用if _,ok := err.(targetErr)判断err类型
          errors.As()是其增强版:error 链上的任一err与targetErr类型相同,即return true

          // 通过例子学习使用errors.As()
          type sqlError struct {
              error
          }
          
          func (e *sqlError) IsNoRows() bool {
              t, ok := e.error.(ErrNoRows)
              return ok && t.IsNoRows()
          }
          
          type ErrNoRows interface {
              IsNoRows() bool
          }
          
          // 返回一个sqlError
          func sqlExec() error {
              return sqlError{}
          }
          
          // errors.WithStack包装sqlError
          func service() error {
              err := sqlExec()
              if err != nil {
                  return errors.WithStack(err)
              }
              
              return nil
          }
          
          func TestErrAs(t *testing.T) {
              err := service()
              
              // 递归使用errors.UnWrap,只要Err链上有一种Err满足类型断言,即返回true
              sr := &sqlError{}
              if errors.As(err, sr) {
                  log.Println("===== errors.As() succeeded =====")
              }
              
              // 经errors.WithStack包装后,不能通过类型断言将当前Err转换成底层Err
              if _, ok := err.(sqlError); ok {
                  log.Println("===== type assert succeeded =====")
              }
          }
          ----------------------------------代码运行结果--------------------------------------------
          === RUN   TestErrAs
          2022/03/25 18:09:02 ===== errors.As() succeeded =====
          

          例子解读:

          因为使用errors.WithStack包装了sqlErrorsqlError位于error链的底层,上层的error已经不再是sqlError类型,所以使用类型断言无法判断出底层的sqlError

          error处理最佳实践

          上面讲了如何定义error类型,如何比较error类型,现在我们谈谈如何在大型项目中做好error处理

          优先处理error

          当一个函数返回一个非空error时,应该优先处理error,忽略它的其他返回值

          只处理error一次

          • 在Golang中,对于每个err,我们应该只处理一次。
          • 要么立即处理err(包括记日志等行为),return nil(把错误吞掉)。此时因为把错误做了降级,一定要小心处理函数返回值。

          比如下面例子json.Marshal(conf)没有return err ,那么在使用buf时一定要小心空指针等错误

          要么return err,在上层处理err

          反例:

          // 试想如果writeAll函数出错,会打印两遍日志
          // 如果整个项目都这么做,最后会惊奇的发现我们在处处打日志,项目中存在大量没有价值的垃圾日志
          // unable to write:io.EOF
          // could not write config:io.EOF
          
          type config struct {}
          
          func writeAll(w io.Writer, buf []byte) error {
              _, err := w.Write(buf)
              if err != nil {
                  log.Println("unable to write:", err)
                  return err
              }
              
              return nil
          }
          
          func writeConfig(w io.Writer, conf *config) error {
              buf, err := json.Marshal(conf)
              if err != nil {
                  log.Printf("could not marshal config:%v", err)
              }
              
              if err := writeAll(w, buf); err != nil {
                  log.Println("count not write config: %v", err)
                  return err
              }
              
              return nil
          }
          

          不要反复包装error

          我们应该包装error,但只包装一次

          上层业务代码建议Wrap error,但是底层基础Kit库不建议

          如果底层基础 Kit 库包装了一次,上层业务代码又包装了一次,就重复包装了 error,日志就会打重

          比如我们常用的sql库会返回sql.ErrNoRows这种预定义错误,而不是给我们一个包装过的 error

          不透明的错误处理

          在大型项目中,推荐使用不透明的错误处理(Opaque errors):不关心错误类型,只关心error是否为nil

          好处:

          耦合小,不需要判断特定错误类型,就不需要导入相关包的依赖。
          不过有时候,这种处理error的方式不够用,比如:业务需要对参数异常error类型做降级处理,打印Warn级别的日志

          type ParamInvalidError struct {
              Desc string
          }
          
          func (e ParamInvalidError) Unwrap() error { return e }
          
          func (e ParamInvalidError) Error() string { return "ParamInvalidError: " + e.Desc }
          
          func (e ParamInvalidError) Is(err error) bool {
              return reflect.TypeOf(err).Name() == reflect.TypeOf(e).Name()
          }
          
          func NewParamInvalidErr(desc string) error {
              return errors.WithStack(&ParamInvalidError{Desc: desc})
          }
          ------------------------------顶层打印日志---------------------------------
          if errors.Is(err, Err.ParamInvalidError{}) {
              logger.Warnf(ctx, "%s", err.Error())
              return
          }
          if err != nil {
              logger.Errorf(ctx, " error:%+v", err)
          }
          

          简化错误处理

          Golang因为代码中无数的if err != nil被诟病,现在我们看看如何减少if err != nil这种代码

          bufio.scan

          CountLines() 实现了"读取内容的行数"功能

          可以利用 bufio.scan() 简化 error 的处理:

          func CountLines(r io.Reader) (int, error) {
              var (
                  br    = bufio.NewReader(r)
                  lines int
                  err   error
              )
              
              for {
                  _, err := br.ReadString('\n')
                  lines++
                  if err != nil {
                      break
                  }
              }
              
              if err != io.EOF {
                  return 0, nilsadwawa 
              }
              
              return lines, nil
          }
          
          func CountLinesGracefulErr(r io.Reader) (int, error) {
              sc := bufio.NewScanner(r)
              
              lines := 0
              for sc.Scan() {
                  lines++
              }
              
              return lines, sc.Err()
          }
          

          bufio.NewScanner() 返回一个 Scanner 对象,结构体内部包含了 error 类型,调用Err()方法即可返回封装好的error

          Golang源代码中蕴含着大量的优秀设计思想,我们在阅读源码时从中学习,并在实践中得以运用

          type Scanner struct {
              r            io.Reader // The reader provided by the client.
              split        SplitFunc // The function to split the tokens.
              maxTokenSize int       // Maximum size of a token; modified by tests.
              token        []byte    // Last token returned by split.
              buf          []byte    // Buffer used as argument to split.
              start        int       // First non-processed byte in buf.
              end          int       // End of data in buf.
              err          error     // Sticky error.
              empties      int       // Count of successive empty tokens.
              scanCalled   bool      // Scan has been called; buffer is in use.
              done         bool      // Scan has finished.
          }
          
          func (s *Scanner) Err() error {
              if s.err == io.EOF {
                  return nil
              }
              return s.err
          }
          

          errWriter

          WriteResponse()函数实现了"构建HttpResponse"功能

          利用上面学到的思路,我们可以自己实现一个errWriter对象,简化对 error 的处理

          type Header struct {
              Key, Value string
          }
          
          type Status struct {
              Code   int
              Reason string
          }
          
          func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
              _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
              if err != nil {
                  return err
              }
              
              for _, h := range headers {
                  _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
                  if err != nil {
                      return err
                  }
              }
              
              if _, err := fmt.Fprintf(w, "\r\n"); err != nil {
                  return err
              }
              
              _, err = io.Copy(w, body)
              return err
          }
          
          type errWriter struct {
              io.Writer
              err error
          }
          
          func (e *errWriter) Write(buf []byte) (n int, err error) {
              if e.err != nil {
                  return 0, e.err
              }
              
              n, e.err = e.Writer.Write(buf)
              
              return n, nil
          }
          
          func WriteResponseGracefulErr(w io.Writer, st Status, headers []Header, body io.Reader) error {
              ew := &errWriter{w, nil}
              
              fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
              
              for _, h := range headers {
                  fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
              }
              
              fmt.Fprintf(w, "\r\n")
              
              io.Copy(ew, body)
              
              return ew.err
          }
          

          何时该用panic

          在 Golang 中panic会导致程序直接退出,是一个致命的错误。

          建议发生致命的程序错误时才使用 panic,例如索引越界、不可恢复的环境问题、栈溢出等等

          小补充

          errors.New()返回的是errorString对象的指针,其原因是防止字符串产生碰撞,如果发生碰撞,两个 error 对象会相等。
          源码:

          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
          }
          

          实践:error1error2的text都是"error",但是二者并不相等

          func TestErrString(t *testing.T) {
              var error1 = errors.New("error")
              var error2 = errors.New("error")
              
              if error1 != error2 {
                  log.Println("error1 != error2")
              }
          }
          ---------------------代码运行结果--------------------------
          === RUN   TestXXXX
          2022/03/25 22:05:40 error1 != error2
          

          参考文献
          《Effective GO》
          《Go程序设计语言》
          https://dave.cheney.net/practical-go/presentations/qcon-china.html#_error_handling

          总结 

          声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。