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 } returnERR: 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替换成错误链上第一个匹配上的// 否则返回falsefunc As(err error, target any) bool // Is: 判断错误链上是否有能匹配上的错误func Is(err, target error) bool// Unwrap 解开一层错误链func Unwrap(err error) error
一点不足:无法直接答应调用栈信息,并且Go团队也没有明确的计划。
github.com/pkg/errors
Cause
方法用来判断底层错误 。
WithMessage
方法仅增加上下文文本信息,不附加调用栈。 如果确定错误已被 Wrap
过或不关心调用栈,可以使用此方法。 注意:不要反复 Wrap
,会导致调用栈重复
Wrap
方法用来包装底层错误,增加上下文文本信息并附加调用栈。 一般用于包装对第三方代码(标准库或第三方库)的调用。
// 这里传递错误信息下方法也有很多种,后续内容会提到。这里仅使用New方法举例。if err := Foo(); err != nil { return errors.New("failed to ...")}if err := Bar(); err != nil { return errors.New("failed to ...")}...0
\
原文:https://juejin.cn/post/7100468654663270414