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 来触发和终止异常处理流程。
错误指的是可能出现问题的地方出现了问题,比如打开一个文件时失败,这种情况在人们的意料之中;而异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。
这里给出一些应抛出异常的场景:
- 空指针引用
- 下标越界
- 除数为 0
- 不应该出现的分支,比如 default
- 输入不应该引起函数错误
在应用开发过程中,通过抛出 panic 异常,程序退出,及时发现问题。在部署以后,要保证程序的持续稳定运行,需要及时通过 recover 捕获异常。在 recover 中,要用合理的方式处理异常,如:
- 打印堆栈的调用信息和业务信息,方便记录和排查问题
- 将异常转换为错误,返回给上层调用者处理
例如:
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")
}