常用的error处理方式避免 if err != nil

What is Error

golang 中error本质上就是一个interface

  1. type error interface {
  2. Error() string
  3. }

The error built-in interface type is the conventional interface for representing an error condition, with the nil value representing no error. error就是普普通通的接口,没有任何特殊的地方;接口为nil则代表没有错误

  1. err1 := errors.New("错误信息1")
  2. err2 := fmt.Errorf("错误信息%d", 2)

Error Type

Sentinel Error

  • eg: io.EOF
  • 必须通过 == 判断
  • 不够灵活,项目中对此会产生依赖

结论:Sentinel Error 尽量避免使用

Error Types

  • eg: os.PathError
  • 使用断言获取自定义的更多的上下文
    1. // package os
    2. type PathError {
    3. Op string
    4. Path string
    5. Err error
    6. }

    结论:需要通过类型switch判断,导致与调用者产生强耦合 Error Types 尽量避免使用

Opaque errors

  • 不透明业务处理
  • 只需要 err != nil 来2分处理
  • 有时候需要对特殊的错误进行判断
    • 定义私有接口
    • 返回实现func 断言成功后返回 ```go type isErr interface { Timeout() bool }

func IsTimeout(err error) bool { e, ok := err.(isErr) return ok && e.Timeout() }

  1. > 结论:不需要了解err的底层结构,只对最终结果行为进行判断
  2. <a name="l47mR"></a>
  3. # Handing Error
  4. - 正常逻辑不缩进代码,保持处理逻辑直线
  5. ```go
  6. // 不推荐
  7. if err == nil {
  8. // do stuff
  9. }
  10. // deal err
  11. // 推荐:
  12. if err != nil {
  13. // deal err
  14. }
  15. // do stuff
  • 不做处理的err,需要直接返回
    ```go // 不推荐 func Auth(r *Request) error { err := authenticate(r.User) if err != nil {
    1. return err
    } return nil }

// 推荐 func Auth(r *Request) error { return authenticate(r.User) }

  1. - io.Reader读取行数
  2. ```go
  3. // 不推荐
  4. func CountLines(r io.Reader) (int, error) {
  5. var err error
  6. sc := bufio.NewScanner(r)
  7. lines := 0
  8. for {
  9. _, err = br.ReadString('\n')
  10. lines++
  11. if err != nil {
  12. break
  13. }
  14. }
  15. if err != io.EOF {
  16. return 0, err
  17. }
  18. return lines, nil
  19. }
  20. // 推荐
  21. func CountLines(r io.Reader) (int, error) {
  22. sc := bufio.NewScanner(r)
  23. lines := 0
  24. for sc.Scan() {
  25. lines++
  26. }
  27. return lines, sc.Err()
  28. }
  • write方法重写 ```go 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 }

  1. for _, h := range headers {
  2. _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
  3. if err != nil {
  4. return err
  5. }
  6. }
  7. if _, err := fmt.Fprintf(w, "\r\n"); err != nil {
  8. return err
  9. }
  10. _, err := io.Copy(w, body)
  11. return err

}

// 推荐 type errWrite struct { io.Writer err error }

func (e *errWrite) Write(buf []byte) (int, error) { if e.err != nil { return 0, e.err } var n int n, e.err = e.Writer.Write(buf) return n, nil }

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { ew := &errWrite{Writer: w} fmt.Fprintf(ew, “HTTP/1.1 %d %s\r\n”, st.Code, st.Reason)

  1. for _, h := range headers {
  2. fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
  3. }
  4. fmt.Fprintf(ew, "\r\n")
  5. io.Copy(ew, body)
  6. return ew.err

}

  1. <a name="Nrpwi"></a>
  2. ## Wrap errors
  3. - 现存的问题:
  4. - 直接返回error错误,不清楚堆栈信息,找不到那个代码触发文件触发的错误
  5. ```go
  6. func Auth(r *Request) error {
  7. return authenticate(r.User)
  8. }
  • 如果需要信息要自行拼接,
    • 没有 file:line 信息
    • 同时由于每一层调用都需要做类似处理,日志会散落各处
    • 同时由于破坏了原有的err信息,不能再次进行等值判断
      1. func Auth(r *Request) error {
      2. err := authenticate(r.User)
      3. if err != nil {
      4. return fmt.Errorf("Auth failed: %v", err)
      5. }
      6. return nil
      7. }
  • 处理原则:
    • 错误要被日志记录
    • 应用处理错误,应该100%保证其完整性
    • 之后不能在报告当前错误
  • 解决实践:

    github.com/pkg/errors

  1. func ReadFile(path string) error {
  2. f, err := os.Open(path)
  3. if err != nil {
  4. return errors.Wrap(err, "open faild")
  5. }
  6. f.Close()
  7. return nil
  8. }
  9. func step1() error {
  10. return errors.WithMessage(ReadFile("xxxxxxx.json"), "step1 err")
  11. }
  12. func step2() error {
  13. return errors.WithMessage(step1(), "step2 err")
  14. }
  15. func main() {
  16. err := step2()
  17. if err != nil {
  18. // %+v 可以格式化打印堆栈信息
  19. fmt.Printf("%+v\n", err)
  20. fmt.Printf(errors.Cause(err).Error())
  21. var pErr *os.PathError
  22. if errors.As(err, &pErr) {
  23. fmt.Println(pErr.Path)
  24. }
  25. os.Exit(1)
  26. }
  27. fmt.Println("suc")
  28. os.Exit(1)
  29. }

image.png

  • Wrap使用原则

    • errors.Wrap 在业务应用调用标准库或基础库时使用
    • errors.Wrap 只需要在一处调用,目的是获取堆栈Caller层级
    • 自己应用内部产生的error不需要使用errors.Wrap
    • 业务应用每处都应该直接返回,而不是每个错误产生到处打日志
    • 程序中间件或goroutine顶部,使用 %+v 打印堆栈详情信息
    • 只有需要处理的时候在判断err是否为nil
    • 一旦err被处理,在向上抛数据则应该返回nil

      Go 1.13

  • 新增 %w err上面追加error

    • fmt.Errorf("someerr: %w", err)
  • Unwarp 解析追加的最底层

    e2 := e1.Unwarp() e1的底层error是e2

  • Is

    • 判断2个err是否相等
  • As
    • error类型映射,如果2个类型相符合,可以直接调用底层err的方法

      1.13 版本不支持堆栈调用 所以不支持Wrap方法