Contents
一、概述
Golang 的错误处理一直是一个比较讨论比较多的话。我刚接触 Golang 的时候也看过关于错误处理的一些文档,但是并没有放在心上。在我使用 Golang 一段时间之后,我觉得我可能无法忽略这个问题。因此,这篇文章主要是为了整理一些在 Golang 中常用的错误处理技巧和原则。
二、错误处理的技巧和原则
2.1 使用封装来避免重复的错误判断
在 Golang 的项目中,最多的一句代码肯定是 if err != nil
。Golang 将错误作为返回值,因此你不得不处理这些错误。但是有的时候,错误处理的判断可能会占据你的代码的一半篇幅,这使得代码看起来乱糟糟的。在官方的博客中有一个这样的例子:
_, err = fd.Write(p0[a:b])
if err != nil {
return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
return err
}
// and so on
是的,你没有看错,这里其实就是调用了 3 行 fd.Write
,但是你不得不写上 9 错误判断。因此官方的博客中也给出了一个比较优雅的处理方案:将 io.Writer
再封装一层。
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
return ew.err
}
现在看上去就好多了,write(buf []byte)
方法在内部判断了错误值,来避免在外面多次的错误判断。当然,这种写法可能也有它的弊端,比如你没有办法知道出错在哪一行调用。大多数情况下,你只需要检查错误,然后进行处理而已。因此这种技巧还是很有用的。Golang 的标准库也有很多类似的技巧。比如
b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// and so on
if b.Flush() != nil {
return b.Flush()
}
其中 b.Write
是有错误值返回的,这只是为了符合 io.Writer
接口。你可以在调用 b.Flush()
的时候再进行错误值的判断。
2.2 Golang 1.13 前的错误处理
检验错误
大多数情况下,我们只需要对错误进行简单的判断即可。因为我们不需要对错误做其他的处理,只需要保证代码逻辑正确执行即可。
if err != nil {
// something went wrong
}
但有的时候我们需要根据错误类型进行不同的处理,比如网络连接未连接/断开导致的错误,我们应该在判断是未连接/断开时,进行重连操作。 在涉及到错误类型的判断时,我们通常有两种方法
- 将错误和已知的值进行比对
var ErrNotFound = errors.New("not found")
if err == ErrNotFound {
// something wasn't found
}
- 判断错误的具体类型
type NotFoundError struct {
Name string
}
func (e *NotFoundError) Error() string { return e.Name + ": not found" }
if e, ok := err.(*NotFoundError); ok {
// e.Name wasn't found
}
添加信息
当一个错误在经过多层的调用栈向上返回时,我们通常会在这个错误上添加一些额外的信息,以帮助开发人员判断错误出现时程序运行到了哪里,发生了什么。最简单的方式是,使用之前的错误信息构造新的错误:
if err != nil {
return fmt.Errorf("decompress %v: %v", name, err)
}
使用 fmt.Errorf
只保留了上一个错误的文本,丢弃了其他所有的信息。如果我们想保留上一个错误的所有信息,我们可以使用下面的方式:
type QueryError struct {
Query string
Err error
}
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
// query failed because of a permission problem
}
2.3 Golang 1.13 中的错误处理
Golang 1.13 中,如果一个错误包含了另一个错误,则可以通过实现 Unwrap()
方法来返回底层的错误。如果 e1.Unwrap()
返回了 e2,我们就可以说 e1 包含了 e2。
使用 Is 和 As 来检验错误
在 2.2 中提到了错误信息的常见处理方式,在 Golang 1.13 中,标准库中添加了几个方法来帮助我们更快速的完成以上的工作。当前前提是,你的自定义 Error 正确的实现了 Unwrap()
方法
errors.Is
用来将一个错误和一个值进行对比:
// Similar to:
// if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {
// something wasn't found
}
errors.As
用来判断一个错误是否是一个特定的类型:
// Similar to:
// if e, ok := err.(*QueryError); ok { … }
var e *QueryError
if errors.As(err, &e) {
// err is a *QueryError, and e is set to the error's value
}
当操作一个包装了的错误时,Is
和 As
会考虑错误链上所有的错误。一个完整的例子如下:
type ErrorA struct {
Msg string
}
func (e *ErrorA) Error() string {
return e.Msg
}
type ErrorB struct {
Msg string
Err *ErrorA
}
func (e *ErrorB) Error() string {
return e.Msg + e.Err.Msg
}
func (e *ErrorB) Unwrap() error {
return e.Err
}
func main() {
a := &ErrorA{"error a"}
b := &ErrorB{"error b", a}
if errors.Is(b, a) {
log.Println("error b is a")
}
var tmpa *ErrorA
if errors.As(b, &tmpa) {
log.Println("error b as ErrorA")
}
}
输出如下:
error b is a
error b as ErrorA
使用 %w 来包装错误
Go 1.13 中增加了 %w,当 %w 出现时,由 fmt.Errorf
返回的错误,将会有 Unwrap
方法,返回的是 %w 对应的值。下面是一个简单的例子:
type ErrorA struct {
Msg string
}
func (e *ErrorA) Error() string {
return e.Msg
}
func main() {
a := &ErrorA{"error a"}
b := fmt.Errorf("new error: %w", a)
if errors.Is(b, a) {
fmt.Println("error b is a")
}
var tmpa *ErrorA
if errors.As(b, &tmpa) {
fmt.Println("error b as ErrorA")
}
}
输出如下:
error b is a
error b as ErrorA
是否需要对错误进行包装
当你向一个 error 中添加额外的上下文信息时,要么使用 fmt.Errorf
,要么实现一个自定义的错误类型,这是你就要决定这个新的错误是否应该包装原始的错误信息。这是一个没有标准答案的问题,它取决于新错误创建的上下文。
包装一个错误是为了将它暴露给调用者。这样调用者就可以根据不同的原始错误作出不同的处理,比如 os.Open(file)
会返回文件不存在这种具体的错误, 这样调用者就可以通过创建文件来让代码可以正确往下执行。
当我们不想暴露实现细节时就不要包装错误。因为暴露一个具备细节的错误,就意味的调用者和我们的代码产生了耦合。这也违反了抽象的原则。