Golang有很多优点,但是Go对错误处理的支持目前并不理想,以至于一直有一个if err != nil的梗流传于Gopher间。即便如此Gopher们也在不断的努力探索着各种优雅的解决方案。笔者就目前一些常用的解决方案进行了总结整理。
函数内错误处理
- 最简单粗暴的办法
通过return方式直接传给调用者进行处理。如:
if err := Foo(); err != nil {
return err
}
然而,在实际使用中我们往往需要将包含一些自定义的错误信息,这是容易形成每一个err都需要判断,并且转载对应错误信息的情况。如:
// 这里传递错误信息下方法也有很多种,后续内容会提到。这里仅使用New方法举例。
if err := Foo(); err != nil {
return errors.New("failed to ...")
}
if err := Bar(); err != nil {
return errors.New("failed to ...")
}
...
这时候我们就会希望能有一个集中处理错误的地方,也就自然而然的产生了下面这几种方法。
return+defer方式
遇到错误直接return,利用defer在函数结束后统一处理。
func SomeFunc() {
var err error
defer func() {
if err != nil {
switch err {
case io.EOF:
fmt.Println(err)
case sql.ErrConnDone:
fmt.Println(err)
default:
fmt.Println("unknown err")
}
return
}
fmt.Println("I'm ok!")
}()
if err = foo(); err != nil {
return
}
if err = bar(); err != nil {
return
}
}
-
goto+Lable方式使用
ERR在代码最后标记一个错误处理代码区,当发生错误时候,使用goto跳转到错误处理区,进行错误的集中处理。而如果代码正常运行,会直接通过return结束,并不会进入错误处理区。
func SomeFunc() {
var err error
if err = foo(); err != nil {
goto ERR
}
if err = bar(); err != nil {
goto ERR
}
return
ERR:
switch err {
case io.EOF:
fmt.Println(err.Error())
case sql.ErrConnDone:
fmt.Println(err.Error())
}
}
panic+recover不推荐
遇到错误直接panic,使用recover捕获
func Run() {
defer func() {
if err := recover(); err != nil {
// 错误处理
}
}()
if err := foo(); err != nil {
panic(err)
}
}
不推荐的原因:
- 混淆了错误和异常的概念。
- 开销远大于之前两种方式。
- 风险大,稍有不甚就会导致整个程序崩溃。
虽说不推荐,但还是把它拿出来讲,是因为这个方式可以不用每个函数都搞一个错误处理区,而是可以在顶层调用者处进行recover,统一处理。
注意: 携程之间的panic是不能通过recover捕捉到的。一个携程因为panic崩溃会导致整个程序崩溃。
func foo() {
panic("foo failed")
}
func Bar() {
panic("bar failed")
}
func SomeFunc() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
foo()
bar()
}
错误信息的携带
主要由两个方向:一个是预定义错误;另一个是自定义错误类。
- 预定义错误
如io包中的一些预定义错误
// ErrShortWrite means that a write accepted fewer bytes than requested
// but failed to return an explicit error.
var ErrShortWrite = errors.New("short write")
// errInvalidWrite means that a write returned an impossible count.
var errInvalidWrite = errors.New("invalid write result")
// ErrShortBuffer means that a read required a longer buffer than was provided.
var ErrShortBuffer = errors.New("short buffer")
// EOF is the error returned by Read when no more input is available.
// (Read must return EOF itself, not an error wrapping EOF,
// because callers will test for EOF using ==.)
// Functions should return EOF only to signal a graceful end of input.
// If the EOF occurs unexpectedly in a structured data stream,
// the appropriate error is either ErrUnexpectedEOF or some other error
// giving more detail.
var EOF = errors.New("EOF")
...
-
自定义错误类
这是最常见的方式。将需要的信息添加到结构体中即可。
type Err struct {
Err error
Msg string
...
}
甚至还可以通过实现error接口,利用官方的error进行传递。
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
实践方案:
注意: 自定义错误类方式往往会存在错误嵌套,我们要尽量减少不必要的包裹。以免造成错误链过长,影响性能。
- Wrap方案
这是Go1.13引入的错误处理方式,据说源自于golang.org/x/xerrors。
Go1.13引入了新的格式化动词: %w,用于实现Wrap效果。
fmt.Errorf("failed to login: %w", err)
// 错误处理可以使用以下方法
// AS: 按顺序寻找错误链中是否有与目标匹配的错误?
// 第二个参数可以是任何实现了error接口的非空类型
// 如果有返回true,并将err替换成错误链上第一个匹配上的
// 否则返回false
func As(err error, target any) bool
// Is: 判断错误链上是否有能匹配上的错误
func Is(err, target error) bool
// Unwrap 解开一层错误链
func Unwrap(err error) error
一点不足:无法直接答应调用栈信息,并且Go团队也没有明确的计划。
-
github.com/pkg/errorsWrap方法用来包装底层错误,增加上下文文本信息并附加调用栈。 一般用于包装对第三方代码(标准库或第三方库)的调用。
WithMessage方法仅增加上下文文本信息,不附加调用栈。 如果确定错误已被Wrap过或不关心调用栈,可以使用此方法。 注意:不要反复Wrap,会导致调用栈重复
Cause方法用来判断底层错误 。
func foo() error {
return errors.Wrap(sql.ErrNoRows, "foo failed")
}
func bar() error {
return errors.WithMessage(foo(), "bar failed")
}
func main() {
err := bar()
if errors.Cause(err) == sql.ErrNoRows {
fmt.Printf("data not found, %v\n", err) // 不会打印调用栈信息
fmt.Printf("%+v\n", err) // 会输出调用栈信息
return
}
if err != nil {
// unknown error
}
}
\