Error vs Exception

error的定义

Go error 就是普通的一个接口,普通的值。

1
2
3
4
5
// 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
}

只要实现了Error()方法就是error.

errors.New() 返回的是内部 errorString 对象的指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
	return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

为什么error.New()要返回指针?我们看下面这个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
	"errors"
	"fmt"
)

// Create a named type for our new errortype.
type errorString string

// Implement the error interface.
func (e errorString) Error() string {
	return string(e)
}

// New creates interface values of type error
func New(text string) error {
	return errorString(text)
}

var ErrNamedType = New("EOF")
var ErrStructType = errors.New("EOF")

func main() {
	if ErrNamedType == New("EOF") {
		fmt.Println("Named Type Error")
	}
	if ErrStructType == errors.New("EOF") {
		fmt.Println("Struct Type Error")
	}
}

我们看一下,上面的error.New()没有返回指针.

假设我们定义两个error对象,他们的文本都叫EOF,ErrNamedType是自定义的,ErrStructType是标准库的.我们运行代码,会返回:

1
Named Type Error

自定义的errorString对象和用自定义方法重新New的新error会返回相等,因为它们底层都是string,如果文本相同,就是相等.

但是对于标准库的errors来说,上述代码没输出,所以说这也是为什么标准库的errors要用&struct来返回,这样的话,每次调用errors.New,都会返回一个新对象.比较标准库的error时,会比较两个error的底层指针是否一致,来判断是否是同一个error.防止出现两个error如果字符串相等,判定就相等的情况.

Error vs Exception

各个语言的演进历史:

  • C:单返回值,一般通过传递指针作为入参,返回值为 int 表示成功还是失败。

  • C++:引入了 exception,但是无法知道被调用方会抛出什么异常。

  • Java:引入了 checked exception,方法的所有者必须申明,调用者必须处理。在启动时抛出大量的异常是司空见惯的事情,并在它们的调用堆栈中尽职地记录下来。Java 异常不再是异常,而是变得司空见惯了。它们从良性到灾难性都有使用,异常的严重性由函数的调用者来区分。

Go 的处理异常逻辑是不引入 exception,支持多参数返回,所以很容易的在函数签名中带上实现了 error interface 的对象,交由调用者来判定。一般最后一个参数是error

如果一个函数返回了 value, error,不能对这个 value 做任何假设,必须先判定 error。唯一可以忽略 error 的是,如果连 value 也不关心。

Go 中有 panic 的机制,如果认为和其他语言的 exception 一样,那就错了。当我们抛出异常的时候,相当于把 exception 扔给了调用者来处理。

比如,在 C++ 中,把 string 转为 int,如果转换失败,会抛出异常。或者在 java 中转换 string 为 date 失败时,会抛出异常。

Go panic 意味着 fatal error(就是挂了)。不能假设调用者来解决 panic,意味着代码不能继续运行。

panic后不能假设调用者会进行recover,然后让代码正确运行.因为抛panic可能是在代码的任意地方,如果recover了panic后,不知道代码运行在什么位置,当继续处理代码时,执行的逻辑不是完整的,不具有代码层面的完整一致性.

这本质上也是其他语言exception的问题.比如调用一个函数,并传入指针,在函数内部对指针进行修改,此时函数抛出exception,我们无法得知此函数对指针的改动,代码逻辑无法继续.

在业务代码中,几乎不可能抛panic.

使用多个返回值和一个简单的约定,Go 解决了让程序员知道什么时候出了问题,并为真正的异常情况保留了 panic。

error的输出格式

基础库中定义了很多包级别的error,我们称之为哨兵error.外面的人只要import这个包,就可以使用error,他们可以进行等值判断.

1
2
3
4
5
6
var (
	ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
	ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
	ErrBufferFull        = errors.New("bufio: buffer full")
	ErrNegativeCount     = errors.New("bufio: negative count")
)

为了保证error输出可以被定位.建议在前面加入包名,比如上述的"bufio: "

返回error

通常情况下,我们不会使用bool来判定函数执行是否正确,而是使用内置error的方式

panic与error

对于真正意外的情况,那些表示不可恢复的程序错误,例如索引越界、不可恢复的环境问题、栈溢出,我们才使用 panic。对于其他的错误情况,我们应该是期望使用 error 来进行判定。

  • 简单。只要接error就能进行错误处理.
  • 考虑失败,而不是成功(Plan for failure, not success)。每一行代码都可能报错,如果能立即处理,会降低心智负担.
  • 没有隐藏的控制流。
  • 完全交给你来控制 error。
  • Error are values。error本身是一种值类型,可以根据需要进行定制.

panic的触发时机

服务的强依赖建议panic.

  • init函数的初始化失败会panic
  • main函数中有一些代码逻辑的初始化是强依赖的,如果初始化不成功,必须panic,只能进程退出,因为强行启动一个服务,有依赖的资源没准备好,去提供服务的话会大量报错.
  • 有些配置文件的值写的不对,比如明显不符合预期,此时需要panic.这样就会避免程序运行在配置出问题的场景,研发排查困难的情况.

服务需要区分哪些是强依赖,哪些是弱依赖,这取决于业务,看下面的例子:

  • 一个读多写少的服务,此时mysql不可用报panic,缓存可用.因为是读多写少,用户请求会大量命中缓存,即使mysql宕机,依然可以保证大部分用户的读请求,此时mysql连不上,不应该panic.mysql就是弱依赖

  • 依赖的底层grpc服务宕机,下游服务发版时初始化grpc.client失败报panic,无法启动.

    • 如果grpc.client是blocking,如果依赖服务宕机,对当前服务有影响,无法初始化.
    • 如果grpc.client是nonblocking,如果依赖服务宕机,服务会大量报错,但是服务能启动,可以正常工作,甚至还能降级.但最开始的一批流量会大量报错.
    • nonblocking+timeout机制,如果时间限制内不成功,就以nonblocking的方式继续listen.这样既尝试连接一次,也不会无限等待,导致服务无法初始化.

我们往往可以通过降级操作将强依赖变为弱依赖,这是工程化的主要解决办法.

Error Type

Sentinel Error

预定义的特定错误,我们叫为 sentinel error,这个名字来源于计算机编程中使用一个特定值来表示不可能进行进一步处理的做法。所以对于 Go,我们使用特定的值来表示错误。

1
if err == ErrSomething {  }

类似的 io.EOF,更底层的 syscall.ENOENT。

使用 sentinel 值是最不灵活的错误处理策略,因为调用方必须使用 == 将结果与预先声明的值进行比较。当您想要提供更多的上下文时,这就出现了一个问题,因为返回一个不同的错误将破坏相等性检查。

甚至是一些有意义的 fmt.Errorf 携带一些上下文,也会破坏调用者的 == ,调用者将被迫查看 error.Error() 方法的输出,以查看它是否与特定的字符串匹配。

  • 不依赖检查 error.Error 的输出。 不应该依赖检测 error.Error 的输出,Error 方法存在于 error 接口主要用于方便程序员使用,但不是程序(编写测试可能会依赖这个返回)。这个输出的字符串用于记录日志、输出到 stdout 等。

  • Sentinel errors 成为你 API 公共部分。 如果您的公共函数或方法返回一个特定值的错误,那么该值必须是公共的,当然要有文档记录,这会增加 API 的表面积。 如果 API 定义了一个返回特定错误的 interface,则该接口的所有实现都将被限制为仅返回该错误,即使它们可以提供更具描述性的错误。 比如 io.Reader。像 io.Copy 这类函数需要 reader 的实现者比如返回 io.EOF 来告诉调用者没有更多数据了,但这又不是错误。

  • Sentinel errors 在两个包之间创建了依赖。 sentinel errors 最糟糕的问题是它们在两个包之间创建了源代码依赖关系。例如,检查错误是否等于 io.EOF,您的代码必须导入 io 包。这个特定的例子听起来并不那么糟糕,因为它非常常见,但是想象一下,当项目中的许多包导出错误值时,存在耦合,项目中的其他包必须导入这些错误值才能检查特定的错误条件(in the form of an import loop)。

结论: 尽可能避免 sentinel errors。

我的建议是避免在编写的代码中使用 sentinel errors。在标准库中有一些使用它们的情况,但这不是一个您应该模仿的模式。

Error Types

Error type 是实现了 error 接口的自定义类型。例如 MyError 类型记录了文件和行号以展示发生了什么。

因为 MyError 是一个 type,调用者可以使用断言转换成这个类型,来获取更多的上下文信息。

与错误值相比,错误类型的一大改进是它们能够包装底层错误以提供更多上下文。 一个不错的例子就是 os.PathError 他提供了底层执行了什么操作、那个路径出了什么问题。

调用者要使用类型断言和类型 switch,就要让自定义的 error 变为 public。这种模型会导致和调用者产生强耦合,从而导致 API 变得脆弱。

结论是尽量避免使用 error types,虽然错误类型比 sentinel errors 更好,因为它们可以捕获关于出错的更多上下文,但是 error types 共享 error values 许多相同的问题。

因此,我的建议是避免错误类型,或者至少避免将它们作为公共 API 的一部分。

Opaque errors

在我看来,这是最灵活的错误处理策略,因为它要求代码和调用者之间的耦合最少。

我将这种风格称为不透明错误处理,因为虽然您知道发生了错误,但您没有能力看到错误的内部。作为调用者,关于操作的结果,您所知道的就是它起作用了,或者没有起作用(成功还是失败)。

这就是不透明错误处理的全部功能–只需返回错误而不假设其内容。

在少数情况下,这种二分错误处理方法是不够的。例如,与进程外的世界进行交互(如网络活动),需要调用方调查错误的性质,以确定重试该操作是否合理。在这种情况下,我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值。考虑这个例子:

net库定义了一个interface

在net库内部进行断言错误类型.

我们使用的时候先判断是否为net.Error,然后判断是否满足错误类型

我们可以定义一个隐藏的interface,对外暴露的是IsTemporary方法,当传入error时,会在内部断言是否是这个error,然后判断是否符合状态.这样temporary也不会暴露出去,更加友好一点.对外不会暴露需要断言的方式,只能内部提供断言方法.

这里的关键是,这个逻辑可以在不导入定义错误的包或者实际上不了解 err 的底层类型的情况下实现——我们只对它的行为感兴趣。

Handling Error

Indented flow is for errors

无错误的正常流程代码,将成为一条直线,而不是缩进的代码。

我们不应该把error在一层层嵌套,应该在err!=nil时马上return掉,正常代码逻辑在同一缩进中写.

Eliminate error handling by eliminating errors

上述代码可以用下面的代码替代,尽量减少代码量

统计 io.Reader 读取内容的行数:

改进版本:

io读写:

我们可以用errWriter的结构体来优化:

errWriter包含了成员变量err,每次调用Write,如果有报错,不会立马返回,把内部的err先保存起来,在下次判断的时候再判断err.

我们用errWriter将Writer包起来,我们不需要在代码过程进行值判定,只需要最终返回ew.err即可.

Wrap Errors

还记得之前我们 auth 的代码吧,如果 authenticate 返回错误,则 AuthenticateRequest 会将错误返回给调用方,调用者可能也会这样做,依此类推。在程序的顶部,程序的主体将把错误打印到屏幕或日志文件中,打印出来的只是:没有这样的文件或目录。

没有生成错误的 file:line 信息。没有导致错误的调用堆栈的堆栈跟踪。这段代码的作者将被迫进行长时间的代码分割,以发现是哪个代码路径触发了文件未找到错误。

但是正如我们前面看到的,这种模式与 sentinel errors 或 type assertions 的使用不兼容,因为将错误值转换为字符串,将其与另一个字符串合并,然后将其转换回 fmt.Errorf 破坏了原始错误,导致等值判定失败。

应该只处理error一次,处理error意味着要去检查错误的值,然后作出决定

我们经常发现类似的代码,在错误处理中,带了两个任务: 记录日志并且再次返回错误。

在这个例子中,如果在 w.Write 过程中发生了一个错误,那么一行代码将被写入日志文件中,记录错误发生的文件和行,并且错误也会返回给调用者,调用者可能会记录并返回它,一直返回到程序的顶部。

会收到2个io.EOF,日志四处乱打,十分不友好.

1
2
unable to write: io.EOF
could not write config: io.EOF

Go 中的错误处理契约规定,在出现错误的情况下,不能对其他返回值的内容做出任何假设。由于 JSON 序列化失败,buf 的内容是未知的,可能它不包含任何内容,但更糟糕的是,它可能包含一个半写的 JSON 片段。

由于程序员在检查并记录错误后忘记 return,损坏的缓冲区将被传递给 WriteAll,这可能会成功,因此配置文件将被错误地写入。但是,该函数返回的结果是正确的。

那么我们在if err !=nil{}中应该写什么?遇到error,要么直接return,要么进行降级处理:将error吞掉,并对value作出处理.否则一定要把错误往上抛.

日志记录与错误无关且对调试没有帮助的信息应被视为噪音,应予以质疑。记录的原因是因为某些东西失败了,而日志包含了答案。

  • 错误要被日志记录。
  • 应用程序处理错误,保证100%完整性,如果吞掉error,一定要对value负起责任.
  • 当前报告错误后,之后不再报告当前错误,只在一处打日志.

可以使用 github.com/pkg/errors

  • Wrap可以将调用处的堆栈信息保存起来,并且可以加上自定义的上下文信息.
  • WithMessage不保存堆栈信息.
  • Cause可以将最底层的error取出.

利用这种wrap的方式,我们可以直接在http或grpc的拦截器打日志,就可以获取到堆栈信息,而不需要在业务代码的各个地方打印error.

通过使用 pkg/errors 包,您可以向错误值添加上下文,这种方式既可以由人也可以由机器检查.人可以阅读错误,机器可以通过cause拿到底层的错误进行判定.

我们在上述代码中完成了两件事:

  1. 记日志
  2. 返回error.

我们用wrap可以一次性完成两个任务:

如何使用pkg/errors

pkg/errors虽然很强,但使用的时候需要注意:

在应用代码中,比如业务程序,而不是基础库,使用 errors.New 或者 errros.Errorf 返回错误。这两个方法都会把堆栈信息返回

如果调用包内其他的函数,通常简单的直接返回,否则会出现两份堆栈信息

如果和其他库,比如Go标准库,公司内部的基础库,开源第三方库进行协作,考虑使用 errors.Wrap 或者 errors.Wrapf 保存堆栈信息。

直接返回错误,而不是每个错误产生的地方到处打日志。

在程序的顶部或者是工作的 goroutine 顶部(请求入口),使用 %+v 把堆栈详情记录。

使用 errors.Cause 获取 root error,再进行和 sentinel error 判定。

总结:

  • Packages that are reusable across many projects only return root error values. 选择 wrap error是只有业务程序可以选择应用的策略。具有最高可重用性的包只能返回根错误值。此机制与 Go 标准库中使用的相同(kit 库的 sql.ErrNoRows)。
  • If the error is not going to be handled, wrap and return up the call stack. 这是关于函数/方法调用返回的每个错误的基本问题。如果函数/方法不打算处理错误,那么用足够的上下文 wrap errors 并将其返回到调用堆栈中。例如,额外的上下文可以是使用的输入参数或失败的查询语句。确定您记录的上下文是足够多还是太多的一个好方法是检查日志并验证它们在开发期间是否为您工作。不建议打印response,因为Response通常很大,我们一般只需要request和error就可以定位问题.
  • Once an error is handled, it is not allowed to be passed up the call stack any longer. 一旦确定函数/方法将处理错误,错误就不再是错误。如果函数/方法仍然需要发出返回,则它不能返回错误值。它应该只返回零(比如降级处理中,你返回了降级数据,然后需要 return nil)。

Go 1.13的错误处理

介绍

在过去的十年中, Go的errors are values的理念在编码实践中运行得也很良好。尽管标准库对错误处理的的支持很少(只有errors.New和fmt.Errorf函数可以用来构造仅包含字符串消息的错误),但是内置的error接口使Go程序员可以添加所需的任何信息。它所需要的只是一个实现Error方法的类型:

1
2
3
4
5
6
type QueryError struct {
    Query string
    Err   error
}

func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }

像这样的错误类型无处不在,它们存储的信息变化很大,从时间戳到文件名再到服务器地址。通常,该信息包括另一个较低级别的错误以提供其他上下文信息。

在Go代码中,使用一个包含了另一个错误的错误类型的模式十分普遍,以至于经过广泛讨论后,Go 1.13为其添加了明确的支持。这篇文章描述了标准库提供的支持:errors包中的三个新功能,以及fmt.Errorf中添加的新格式化动词。

在详细描述这些变化之前,让我们先回顾一下在Go语言的早期版本中如何检查和构造错误。

Go 1.13版本之前的错误处理

检查错误

错误是值(errors are values)。程序通过几种方式基于这些值来做出决策。最常见的是通过与nil的比较来确定操作是否失败。

1
2
3
if err != nil {
    // 出错了!
}

有时我们将错误与已知的前哨值(sentinel value)进行比较来查看是否发生了特定错误。比如:

1
2
3
4
5
var ErrNotFound = errors.New("not found")

if err == ErrNotFound {
    // something wasn't found
}

错误值可以是满足语言定义的error 接口的任何类型。程序可以使用类型断言(type assertion)或类型开关(type switch)来判断错误值是否可被视为特定的错误类型。

1
2
3
4
5
6
7
8
9
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
}

添加信息

函数通常在将错误向上传递给调用堆栈时添加额外错误信息,例如对错误发生时所发生情况的简短描述。一种简单的方法是构造一个新错误,并在其中包括上一个错误:

1
2
3
if err != nil {
    return fmt.Errorf("decompress %v: %v", name, err)
}

使用fmt.Errorf创建的新错误将丢弃原始错误中的所有内容(文本除外)。就像我们在前面所看到的QueryError那样,有时我们可能想要定义一个包含基础错误的新错误类型,并将其保存下来以供代码检查。我们再次来看一下QueryError:

1
2
3
4
type QueryError struct {
    Query string
    Err   error
}

程序可以查看一个*QueryError值的内部以根据潜在的错误进行决策。有时您会看到称为“展开”错误的信息。

1
2
3
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
    // query failed because of a permission problem
}

标准库中的os.PathError类型就是另外一个在错误中包含另一个错误的示例。

Go 1.13版本的错误处理

Unwrap方法

Go 1.13在errors和fmt标准库包中引入了新功能以简化处理包含其他错误的错误。其中最重要的不是改变,而是一个约定:包含另一个错误的错误可以实现Unwrap方法来返回所包含的底层错误。如果e1.Unwrap()返回了e2,那么我们说e1包装了e2,您可以Unwrap e1来得到e2。

遵循此约定,我们可以为上面的QueryError类型提供一个Unwrap方法来返回其包含的错误:

1
func (e *QueryError) Unwrap() error { return e.Err }

Unwrap错误的结果本身(底层错误)可能也具有Unwrap方法。我们将这种通过重复unwrap而得到的错误序列为错误链。

以上这个例子,通过errors.Unwrap(w)后,返回的其实是个e,也就是被嵌套的那个error。 这里需要注意的是,嵌套可以有很多层,我们调用一次errors.Unwrap函数只能返回最外面的一层error,如果想获取更里面的,需要调用多次errors.Unwrap函数。最终如果一个error不是warpping error,那么返回的是nil。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func Unwrap(err error) error {
    //先判断是否是wrapping error
	u, ok := err.(interface {
		Unwrap() error
	})
	//如果不是,返回nil
	if !ok {
		return nil
	}
	//否则则调用该error的Unwrap方法返回被嵌套的error
	return u.Unwrap()
}

看看该函数的的源代码吧,这样就会理解的更深入一些,我加了一些注释。

使用Is和As检查错误

Go 1.13的errors包中包括了两个用于检查错误的新函数:Is和As。

errors.Is函数将错误与值进行比较。

1
2
3
4
5
// Similar to:
//   if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {
    // something wasn't found
}

As函数用于测试错误是否为特定类型。

1
2
3
4
5
6
// 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
}

在最简单的情况下,errors.Is函数的行为类似于上面对哨兵错误(sentinel error))的比较,而errors.As函数的行为类似于类型断言(type assertion)。但是,在处理包装错误(包含其他错误的错误)时,这些函数会考虑错误链中的所有错误。让我们再次看一下通过展开QueryError以检查潜在错误:

1
2
3
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
    // query failed because of a permission problem
}

使用errors.Is函数,我们可以这样写:

1
2
3
if errors.Is(err, ErrPermission) {
    // err, or some error that it wraps, is a permission problem
}

errors包还包括一个新Unwrap函数,该函数返回调用错误Unwrap方法的结果,或者当错误没有Unwrap方法时返回nil。通常我们最好使用errors.Is或errors.As,因为这些函数将在单个调用中检查整个错误链。

Is是很简单的一个函数,要么咱俩相等,要么err包含target,这两种情况都返回true,其余返回false。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
func Is(err, target error) bool {
	if target == nil {
		return err == target
	}

	isComparable := reflectlite.TypeOf(target).Comparable()

	//for循环,把err一层层剥开,一个个比较,找到就返回true
	for {
		if isComparable && err == target {
			return true
		}
		//这里意味着你可以自定义error的Is方法,实现自己的比较代码
		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
			return true
		}
		//剥开一层,返回被嵌套的err
		if err = Unwrap(err); err == nil {
			return false
		}
	}
}

Is函数源代码如上,其实就是一层层反嵌套,剥开然后一个个的和target比较,相等就返回true。

从功能上来看,As所做的就是遍历err嵌套链,从里面找到类型符合的error,然后把这个error赋予target,这样我们就可以使用转换后的target了,这里有值的赋予,所以target必须是一个指针。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
func As(err error, target interface{}) bool {
    //一些判断,保证target,这里是不能为nil
	if target == nil {
		panic("errors: target cannot be nil")
	}
	val := reflectlite.ValueOf(target)
	typ := val.Type()

	//这里确保target必须是一个非nil指针
	if typ.Kind() != reflectlite.Ptr || val.IsNil() {
		panic("errors: target must be a non-nil pointer")
	}

	//这里确保target是一个接口或者实现了error接口
	if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
		panic("errors: *target must be interface or implement error")
	}
	targetType := typ.Elem()
	for err != nil {
	    //关键部分,反射判断是否可被赋予,如果可以就赋值并且返回true
	    //本质上,就是类型断言,这是反射的写法
		if reflectlite.TypeOf(err).AssignableTo(targetType) {
			val.Elem().Set(reflectlite.ValueOf(err))
			return true
		}
		//这里意味着你可以自定义error的As方法,实现自己的类型断言代码
		if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
			return true
		}
		//这里是遍历error链的关键,不停的Unwrap,一层层的获取err
		err = Unwrap(err)
	}
	return false
}

这是As函数的源代码,看源代码比较清晰一些,我在代码里做了注释,这里就不一一分析了,大家可以结合注释读一下。

用%w包装错误

如前面所述,我们通常使用fmt.Errorf函数向错误添加其他信息。

1
2
3
if err != nil {
    return fmt.Errorf("decompress %v: %v", name, err)
}

在Go 1.13中,fmt.Errorf函数支持新的%w动词。当存在该动词时,所返回的错误fmt.Errorf将具有Unwrap方法,该方法返回参数%w对应的错误。%w对应的参数必须是错误(类型)。在所有其他方面,%w与%v等同。

1
2
3
4
if err != nil {
    // Return an error which unwraps to err.
    return fmt.Errorf("decompress %v: %w", name, err)
}

使用%w创建的包装错误可用于errors.Is和errors.As:

1
2
3
err := fmt.Errorf("access denied: %w, ErrPermission)
...
if errors.Is(err, ErrPermission) ...

按照这种不丢失原error的思路,那么Wrapping Error的实现原理应该类似我们上面的自定义error.我们看下fmt.Errorf函数的源代码验证下我们的猜测是否正确。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func Errorf(format string, a ...interface{}) error {
	//省略无关代码
	var err error
	if p.wrappedErr == nil {
		err = errors.New(s)
	} else {
		err = &wrapError{s, p.wrappedErr}
	}
	p.free()
	return err
}

这里的关键核心代码就是p.wrappedErr的判断,这个值是否存在,决定是否要生成一个wrapping error。这个值是怎么来的呢?就是根据我们设置的%w解析出来的。

有了这个值之后,就生成了一个&wrapError{s, p.wrappedErr}返回了,这里有个结构体wrapError

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type wrapError struct {
	msg string
	err error
}

func (e *wrapError) Error() string {
	return e.msg
}

func (e *wrapError) Unwrap() error {
	return e.err
}

如上所示,和我们想的一样。实现了Error方法说明它是一个error。Unwrap方法是一个特别的方法,所有的wrapping error 都会有这么一个方法,用于获得被嵌套的error。

是否包装

在使用fmt.Errorf或通过实现自定义类型将其他上下文添加到错误时,您需要确定新错误是否应该包装原始错误。这个问题没有统一答案。它取决于创建新错误的上下文。包装错误将会被公开给调用者。如果要避免暴露实现细节,那么请不要包装错误。

举一个例子,假设一个Parse函数从io.Reader读取复杂的数据结构。如果发生错误,我们希望报告发生错误的行号和列号。如果从io.Reader读取时发生错误,我们将包装该错误以供检查底层问题。由于调用者为函数提供了io.Reader,因此有理由公开它产生的错误。

相反,一个对数据库进行多次调用的函数可能不应该将其中调用之一的结果解开的错误返回。如果该函数使用的数据库是实现细节,那么暴露这些错误就是对抽象的违反。例如,如果你的程序包pkg中的函数LookupUser使用了Go的database/sql程序包,则可能会遇到sql.ErrNoRows错误。如果使用fmt.Errorf(“accessing DB: %v”, err)来返回该错误,则调用者无法检视到内部的sql.ErrNoRows。但是,如果函数使用fmt.Errorf(“accessing DB: %w”, err)返回错误,则调用者可以编写下面代码:

1
2
err := pkg.LookupUser(...)
if errors.Is(err, sql.ErrNoRows) 

此时,如果您不希望对客户端源码产生影响,该函数也必须始终返回sql.ErrNoRows,即使您切换到其他数据库程序包。换句话说,包装错误会使该错误成为您API的一部分。如果您不想将来将错误作为API的一部分来支持,则不应包装该错误。

重要的是要记住,无论是否包装错误,错误文本都将相同。那些试图理解错误的人将得到相同的信息,无论采用哪种方式; 是否要包装错误的选择是关于是否要给程序提供更多信息,以便他们可以做出更明智的决策,还是保留该信息以保留抽象层。

使用Is和As方法自定义错误测试

errors.Is函数检查错误链中的每个错误是否与目标值匹配。默认情况下,如果两者相等,则错误与目标匹配。另外,链中的错误可能会通过实现Is方法来声明它与目标匹配。

例如,下面的错误类型定义是受Upspin error包的启发,它将错误与模板进行了比较,并且仅考虑模板中非零的字段:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
type Error struct {
    Path string
    User string
}

func (e *Error) Is(target error) bool {
    t, ok := target.(*Error)
    if !ok {
        return false
    }
    return (e.Path == t.Path || t.Path == "") &&
           (e.User == t.User || t.User == "")
}

if errors.Is(err, &Error{User: "someuser"}) {
    // err's User field is "someuser".
}

同样,errors.As函数将使用链中某个错误的As方法,如果该错误实现了As方法。

错误和包API

返回错误的程序包(大多数都会返回错误)应描述程序员可能依赖的那些错误的属性。一个经过精心设计的程序包也将避免返回带有不应依赖的属性的错误。

最简单的规约是用于说明操作成功或失败的属性,分别返回nil或non-nil错误值。在许多情况下,不需要进一步的信息了。

如果我们希望函数返回可识别的错误条件,例如“item not found”,则可能会返回包装哨兵的错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
var ErrNotFound = errors.New("not found")

// FetchItem returns the named item.
//
// If no item with the name exists, FetchItem returns an error
// wrapping ErrNotFound.
func FetchItem(name string) (*Item, error) {
    if itemNotFound(name) {
        return nil, fmt.Errorf("%q: %w", name, ErrNotFound)
    }
    // ...
}

还有其他现有的提供错误的模式,可以由调用方进行语义检查,例如直接返回哨兵值,特定类型或可以使用谓词函数检查的值。

在所有情况下,都应注意不要向用户公开内部细节。正如我们在上面的“是否要包装”中提到的那样,当您从另一个包中返回错误时,应该将错误转换为不暴露基本错误的形式,除非您愿意将来再返回该特定错误。

1
2
3
4
5
6
7
8
f, err := os.Open(filename)
if err != nil {
    // The *os.PathError returned by os.Open is an internal detail.
    // To avoid exposing it to the caller, repackage it as a new
    // error with the same text. We use the %v formatting verb, since
    // %w would permit the caller to unwrap the original *os.PathError.
    return fmt.Errorf("%v", err)
}

如果将函数定义为返回包装某些标记或类型的错误,请不要直接返回基础错误。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
var ErrPermission = errors.New("permission denied")

// DoSomething returns an error wrapping ErrPermission if the user
// does not have permission to do something.
func DoSomething() {
    if !userHasPermission() {
        // If we return ErrPermission directly, callers might come
        // to depend on the exact error value, writing code like this:
        //
        //     if err := pkg.DoSomething(); err == pkg.ErrPermission { … }
        //
        // This will cause problems if we want to add additional
        // context to the error in the future. To avoid this, we
        // return an error wrapping the sentinel so that users must
        // always unwrap it:
        //
        //     if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... }
        return fmt.Errorf("%w", ErrPermission)
    }
    // ...
}

参考

http://lessisbetter.site/2018/10/24/go-handle-error/ https://windmt.com/2017/07/30/golang-error-handle/ https://www.flysnow.org/2019/01/01/golang-error-handle-suggestion.html https://my.oschina.net/chai2010/blog/117923 Go 1.13中的错误处理 Go语言(golang)新发布的1.13中的Error Wrapping深度分析