乐趣区

golang优雅的错误处理

golang 的错误处理一直深受大家诟病,项目里面一半的代码在做错误处理。

自己在做 golang 开发一段时间后,也深有同感,觉得很有必要优化一下,一方面让代码更优雅一些,另一方面也为了形成系统的错误处理方式,而不是随心所欲的来个 errors.new(),或者一直 return err。

在查阅一些资料之后,发现自己对 golang 错误处理的认识,还停留在一个低阶的层面上。这里想和大家探讨一下,也为巩固自己所学

错误的返回处理

在函数多层调用时,我常用的处理方式是:

func Write(w io.Writer, buf []byte) error {_, err := w.Write(buf)
  if err != nil {
    // annotated error goes to log file
    log.Println("unable to write:", err)
  
    // unannotated error returned to caller
    return err
  }
  return nil
}

层层都加日志非常方便故障定位,但这样做,日志文件中会有很多重复的错误描述,并且上层调用函数拿到的错误,还是底层函数返回的 error,没有上下文信息

优化一:

func Write(w io.Writer, buf []byte) error {_, err := w.Write(buf)
  if err != nil {
    // annotated error returned to caller
    fmt.Errorf("authenticate failed: %v", err)
  }
  return nil
}

这里去除了重复的错误日志,并且在返回给上层调用函数的 error 信息中加上了上下文信息。但是这样做破坏了相等性检测,即我们无法判断错误是否是一种预先定义好的错误。

例如:

func main() {err := readfile(“.bashrc”)
    if strings.Contains(error.Error(), "not found") {// handle error}
}

func readfile(path string) error {err := openfile(path)
    if err != nil {return fmt.Errorf(“cannot open file: %v", err)
    }
    // ……
}

造成的后果时,调用者不得不用字符串匹配的方式判断底层函数 readfile 是不是出现了某种错误。

优化二:

使用第三方库: github.com/pkg/errors,wrap 可以将一个错误加上一段字符串,包装成新的字符串。cause 进行相反的操作。

// Wrap annotates cause with a message.
func Wrap(cause error, message string) error
// Cause unwraps an annotated error.
func Cause(err error) error

例如:

func ReadFile(path string) ([]byte, error) {f, err := os.Open(path)
    if err != nil {return nil, errors.Wrap(err, "open failed")
    }
    defer f.Close()
    
    buf, err := ioutil.ReadAll(f)
    if err != nil {return nil, errors.Wrap(err, "read failed")
    }
    return buf, nil
}

通过 wrap 即可以包含底层被调用函数的上下文信息,又可以通过 cause 还原错误,对原错误类型进行判断,如下:

func main() {_, err := ReadFile()
    if errors.Cause(err) != io.EOF {fmt.Println(err)
        os.Exit(1)
    }
}

今年刚发布的 go1.13 新增了类似的错误处理函数

//go1.13 没有提供 wrap 函数,但通过 fmt.Errof 提供了类似的功能
fmt.Errorf("context info: %w",err)

// 将嵌套的 error 解析出来,多层嵌套需要调用 Unwrap 函数多次,才能获取最里层的 error
func Unwrap(err error) error

异常

部分开发者写代码中,没有区分异常和错误,都统一按错误来处理,这种方式是不优雅的。要灵活使用 Golang 的内置函数 panic 和 recover 来触发和终止异常处理流程。

错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。

这里给出一些应抛出异常的场景:

  1. 空指针引用
  2. 下标越界
  3. 除数为 0
  4. 不应该出现的分支,比如 default
  5. 输入不应该引起函数错误

在应用开发过程中,通过抛出 panic 异常,程序退出,及时发现问题。在部署以后,要保证程序的持续稳定运行,需要及时通过 recover 捕获异常。在 recover 中,要用合理的方式处理异常,如:

  1. 打印堆栈的调用信息和业务信息,方便记录和排查问题
  2. 将异常转换为错误,返回给上层调用者处理

例如:

func funcA() (err error) {defer func() {if p := recover(); p != nil {fmt.Println("panic recover! p:", p)
            str, ok := p.(string)
            if ok {err = errors.New(str)
            } else {err = errors.New("panic")
            }
            debug.PrintStack()}
    }()
    return funcB()}

func funcB() error {
    // simulation
    panic("foo")
    return errors.New("success")
}
退出移动版