关于golang:一篇文章搞懂Golang的error处理

37次阅读

共计 9177 个字符,预计需要花费 23 分钟才能阅读完成。

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

正文完
 0