golang优雅的错误处理

 阅读约 7 分钟

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")
}
阅读 2.4k发布于 2019-09-18

推荐阅读
目录