Go中Error的处理

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
}

errors包中的error

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
}

返回一个errorString结构体的指针,指针实现errorinterface

基础库中大量自定义的error

1
2
3
// ErrTooLarge is passed to panic if memory cannot be allocated to store data in a buffer.
var ErrTooLarge = errors.New("bytes.Buffer: too large")
var errNegativeRead = errors.New("bytes.Buffer: reader returned negative count from Read")

指针

1
2
3
func New(text string) error {
return &errorString{text}
}

errors.New()中返回的是指针,如果不使用指针,则会出现相同的字符串,会判断成两个error相等。

例如只使用一个字符串作为对象:

image-20230726160017104
1
2
Output:
Named Type Error

例如使用结构体,但是依然不使用指针作为载体:

image-20230726160154049
1
2
Output:
Error: EOF

当使用对象进行比较时,会比较结构体中的字段(前提是结构体中不包含不能比较的字段,例如mapslicechannel),如果每个字段都相等,则代表这两个对象相等。

当取地址进行比较时,会比较指针所指向的内存地址,可以唯一标识这个error。

Error vs Exception

各个语言的演进历史:

  • C

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

    1
    ngx_int_t ngx_create_path(ngx_file_t *file, ngx_path_t *path);
  • C++

    引入exception,但是无法知道被调用方会抛出什么异常

  • Java

    与C一样,单返回值,引入checked exception,方法的所有者必须申明,调用者必须处理。

    在启动时排除大量的异常是司空见惯的事情,并在它们的调用堆栈中尽职地记录下来。

    Java异常不再是异常,而是司空见惯。从良性到灾难性都有使用,异常的严重性由函数的调用者来区分。

    1
    java.Error & java.RuntimeException

Go 处理异常逻辑是不引入 exception ,支持多参数返回,所以很容易实现error interface的对象,交由调用者来判定。

如果一个函数返回(valueerror)(一般error放在最后),通常不能直接使用value,而是必须先判断error。当errornil时,才可以使用value

Go中有panic的机制,而且也不与其他语言的exception一样。当抛出异常时,相当于把exception扔给了调用者来处理。Go中的panic是灾难性的,而不是一个良性的exception

Go panic以为 fatal error(严重性问题,服务挂掉)。不能假设调用者来解决panic,意味着代码不能继续运行。

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

应该返回panic的情况:

  • 强依赖下游服务,并且下游服务异常,在启动时应该panic
  • 配置文件中的配置不是一个合理值,在启动时可以panic,避免使用时无法定位报错位置
  • init函数中报错,避免无法定位报错

Panic

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

you only need to check the error value if you care about the result. – Dave

This blog post from Microsoft’s engineering blog in 2005 still holds true today, namely:

My point isn’t that exceptions are bad. My point is that exceptions are too hard and I’m not smart enough to handle them.

Go使用error的优势:

  • 简单,通过error可以直观的判断逻辑是否错误
  • 考虑失败,而不是成功(plan for failure,not success),优先处理error
  • 没有隐藏的控制流,代码层面直观,不像try-cache
  • 完全交给开发控制error,针对自身逻辑实现error判断控制流程
  • Error are values,在Go中,error也是一个值

Error Type

通过自定义内部错误,可以很好的用来处理error

特定错误

Sentinel Error

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

1
2
3
if err == ErrSomething {
...
}

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

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

甚至一些有意义的 fmt.Errorf 携带一些上下文,也会破坏调用者的 == ,调用者被迫查看 err.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 接口的自定义错误类型,例如以下代码

image-20230727171615286

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

image-20230727171741749

与错误值相比,错误类型的一大改进是它们能够包装底层错误,以提供更多的上下文。

例如下面这个例如,os.PathError 提供了底层执行了什么操作、哪个路径有什么问题

image-20230727171907207

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

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

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

非透明的错误

Opaque errors

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

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

这就是不透明错误处理的全部功能-只需返回错误,而不加设其内容。调用者只需要判断error是否为nil

image-20230727172547821
  • Assert errors for behavior, not type

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

例如:

go/src/net/net.go

自定义Error接口

1
2
3
4
5
6
7
8
9
10
// An Error represents a network error.
type Error interface {
error
Timeout() bool // Is the error a timeout?

// Deprecated: Temporary errors are not well-defined.
// Most "temporary" errors are timeouts, and the few exceptions are surprising.
// Do not use this method.
Temporary() bool
}

接口实现

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
type timeout interface {
Timeout() bool
}

func (e *OpError) Timeout() bool {
if ne, ok := e.Err.(*os.SyscallError); ok {
t, ok := ne.Err.(timeout)
return ok && t.Timeout()
}
t, ok := e.Err.(timeout)
return ok && t.Timeout()
}

type temporary interface {
Temporary() bool
}

func (e *OpError) Temporary() bool {
// Treat ECONNRESET and ECONNABORTED as temporary errors when
// they come from calling accept. See issue 6163.
if e.Op == "accept" && isConnError(e.Err) {
return true
}

if ne, ok := e.Err.(*os.SyscallError); ok {
t, ok := ne.Err.(temporary)
return ok && t.Temporary()
}
t, ok := e.Err.(temporary)
return ok && t.Temporary()
}

使用

go/src/net/http/server.go

1
2
3
4
5
6
7
8
9
10
11
12
13
if ne, ok := err.(net.Error); ok && ne.Temporary() {
if tempDelay == 0 {
tempDelay = 5 * time.Millisecond
} else {
tempDelay *= 2
}
if max := 1 * time.Second; tempDelay > max {
tempDelay = max
}
srv.logf("http: Accept error: %v; retrying in %v", err, tempDelay)
time.Sleep(tempDelay)
continue
}

优秀的Error处理方式

缩进式

Indented flow is for errors

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

image-20230727195737286

正常情况下,应该是正常逻辑的代码,成为一条直线,当有error出现时,这条直线出现分叉。

消除错误

eliminate error handling by eliminating errors

通过消除错误,或者减少对错误的判断,可以达到减少代码的目的

image-20230727195831152

代码可以缩写,直接返回,不用判断

image-20230727195837322

官方处理error的demo:

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

image-20230727195953936

粗看没有问题,但是可以优化

image-20230727200215714

go/src/bufio/scan.go

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
func (s *Scanner) Scan() bool {
if s.done {
return false
}
s.scanCalled = true
// Loop until we have a token.
for {
// See if we can get a token with what we already have.
// If we've run out of data but have an error, give the split function
// a chance to recover any remaining, possibly empty token.
if s.end > s.start || s.err != nil {
advance, token, err := s.split(s.buf[s.start:s.end], s.err != nil)
if err != nil {
if err == ErrFinalToken {
s.token = token
s.done = true
return true
}
s.setErr(err)
return false
}
if !s.advance(advance) {
return false
}
s.token = token
if token != nil {
if s.err == nil || advance > 0 {
s.empties = 0
} else {
// Returning tokens not advancing input at EOF.
s.empties++
if s.empties > maxConsecutiveEmptyReads {
panic("bufio.Scan: too many empty tokens without progressing")
}
}
return true
}
}
// We cannot generate a token with what we are holding.
// If we've already hit EOF or an I/O error, we are done.
if s.err != nil {
// Shut it down.
s.start = 0
s.end = 0
return false
}
// Must read more data.
// First, shift data to beginning of buffer if there's lots of empty space
// or space is needed.
if s.start > 0 && (s.end == len(s.buf) || s.start > len(s.buf)/2) {
copy(s.buf, s.buf[s.start:s.end])
s.end -= s.start
s.start = 0
}
// Is the buffer full? If so, resize.
if s.end == len(s.buf) {
// Guarantee no overflow in the multiplication below.
const maxInt = int(^uint(0) >> 1)
if len(s.buf) >= s.maxTokenSize || len(s.buf) > maxInt/2 {
s.setErr(ErrTooLong)
return false
}
newSize := len(s.buf) * 2
if newSize == 0 {
newSize = startBufSize
}
if newSize > s.maxTokenSize {
newSize = s.maxTokenSize
}
newBuf := make([]byte, newSize)
copy(newBuf, s.buf[s.start:s.end])
s.buf = newBuf
s.end -= s.start
s.start = 0
}
// Finally we can read some input. Make sure we don't get stuck with
// a misbehaving Reader. Officially we don't need to do this, but let's
// be extra careful: Scanner is for safe, simple jobs.
for loop := 0; ; {
n, err := s.r.Read(s.buf[s.end:len(s.buf)])
if n < 0 || len(s.buf)-s.end < n {
s.setErr(ErrBadReadCount)
break
}
s.end += n
if err != nil {
s.setErr(err)
break
}
if n > 0 {
s.empties = 0
break
}
loop++
if loop > maxConsecutiveEmptyReads {
s.setErr(io.ErrNoProgress)
break
}
}
}
}

Scanner 中包含错误,Scan()可以返回是否含有下一行。

将数据写到 io.Writer

image-20230727200320199

也可以再优化,将io.Writererror 合并在一起

image-20230727200519358

后续使用时,就可以不需要判断error,直接将error返回,交给上层一次性判断即可

image-20230727200728044

包装错误

Wrap errors

在一层一层调用时,出现报错时,可能直接返回错误信息,但是无法定位报错位置。

例如下面的代码如果调用者发现有error,但是errro其实是authenticate抛出来的,而且最底层的位置也无法获取。

image-20230727203215477

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

image-20230727203429712

但是这个做法违反了前面 sentinel errorstype assertions 的使用原则。将错误值转换为字符串,将其与另一个字符串合并,然后返回 fmt.Errorf 破坏了原始错误,导致等值判定失效。

you should only handle errors once. Handling an error means inspecting the error value, and making a single decision.

image-20230727203621081

在错误处理中,带了两个任务:记录日志并且再次返回错误

image-20230727203658059

如果上层调用时,调用者也打印日志

image-20230727203804797

或者更难以确定的是main中也打印错误日志

image-20230727203845864

此时会有大量的重复日志打印出来:

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

Go 中的错误处理契约规定,在出现错误的情况下,不能对其他返回值的内容做出任何加设。

例如上面 JSON 序列化失败,buf的内容是未知的,可能它不包含任何内容,但更糟糕的是,它可能包含一个半写的 JSON 片段。

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

image-20230727204131668

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

  • The error has been logged. 错误要被日志记录
  • The application is back to 100% integrity. 应用程序处理错误,保证100%完整性
  • The current error is not reported any longer. 之后不再报告当前错误

这里推荐使用

1
github.com/pkg/errors

最底层的error,通过Wrap包装一下

image-20230727204400110

上层解开

image-20230727204454692

源代码

封装堆栈

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
35
36
37
// WithStack annotates err with a stack trace at the point WithStack was called.
// If err is nil, WithStack returns nil.
func WithStack(err error) error {
if err == nil {
return nil
}
return &withStack{
err,
callers(),
}
}

type withStack struct {
error
*stack
}

func (w *withStack) Cause() error { return w.error }

// Unwrap provides compatibility for Go 1.13 error chains.
func (w *withStack) Unwrap() error { return w.error }

func (w *withStack) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprintf(s, "%+v", w.Cause())
w.stack.Format(s, verb)
return
}
fallthrough
case 's':
io.WriteString(s, w.Error())
case 'q':
fmt.Fprintf(s, "%q", w.Error())
}
}

使用更多字符串信息和堆栈封装

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
// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: message,
}
return &withStack{
err,
callers(),
}
}

// Wrapf returns an error annotating err with a stack trace
// at the point Wrapf is called, and the format specifier.
// If err is nil, Wrapf returns nil.
func Wrapf(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
err = &withMessage{
cause: err,
msg: fmt.Sprintf(format, args...),
}
return &withStack{
err,
callers(),
}
}

只使用错误信息格式化

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// WithMessage annotates err with a new message.
// If err is nil, WithMessage returns nil.
func WithMessage(err error, message string) error {
if err == nil {
return nil
}
return &withMessage{
cause: err,
msg: message,
}
}

// WithMessagef annotates err with the format specifier.
// If err is nil, WithMessagef returns nil.
func WithMessagef(err error, format string, args ...interface{}) error {
if err == nil {
return nil
}
return &withMessage{
cause: err,
msg: fmt.Sprintf(format, args...),
}
}

type withMessage struct {
cause error
msg string
}

func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() }
func (w *withMessage) Cause() error { return w.cause }

// Unwrap provides compatibility for Go 1.13 error chains.
func (w *withMessage) Unwrap() error { return w.cause }

func (w *withMessage) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
if s.Flag('+') {
fmt.Fprintf(s, "%+v\n", w.Cause())
io.WriteString(s, w.msg)
return
}
fallthrough
case 's', 'q':
io.WriteString(s, w.Error())
}
}

解封装,获取最原始的error

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
// Cause returns the underlying cause of the error, if possible.
// An error value has a cause if it implements the following
// interface:
//
// type causer interface {
// Cause() error
// }
//
// If the error does not implement Cause, the original error will
// be returned. If the error is nil, nil will be returned without further
// investigation.
func Cause(err error) error {
type causer interface {
Cause() error
}

for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}

例如实例代码最终打印结果

image-20230728104639631

image-20230728104656500

使用Wrap,可以携带上下文返回,而不是直接处理error,使用起来更加简便

image-20230728104828955

使用Wrap的技巧

  • 在应用代码中(程序代码,而非引用库),使用 errors.New 或者 errors.Errorf 返回错误

    image-20230728104921541

  • 如果调用其他的函数(包内),通常简单的直接返回,因为其他函数的error已经wrap

    image-20230728114315680

  • 如果和其他库进行协作(第三方库),考虑使用 errors.Wrap 或者 errors.Wrapf 保存堆栈信息。同样适用于和标准库协作的时候

    image-20230728114414188

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

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

    image-20230728114523865

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

总结

  • Packages that are reusable across many projects only return root error values

    如果包是一个第三方的包,那么不应该wrap error

    选择wrap error是只有 applications 可以选择应用的策略。具有最高可重用性的包职能返回根错误值。此机制与 Go 标准库中使用的相同。(kit 库的 sql.ErrNoRows

  • If the error is not going to be handled,wrap and return up the call stack

    如果不打算处理error,那么wrap信息,并且往上抛

    这是关于函数、方法调用返回的每个错误的基本问题。如果函数、方法不打算处理错误,那么足够的上下文 wrap errors并将其返回到 调用堆栈中。例如,额外的上下文可以是使用的输入参数或失败的查询语句。确定记录的上下文足够多还是太多的一个方法是检查日志并验证在开发期间是否提供信息

  • Once an error is handled, it is not allowed to be passed up the call stack any longer.

    一旦确定函数、方法将处理错误,错误就不再是错误,如果函数、方法仍需要返回响应,则它不能返回错误,它应该只返回零(比如降级处理中,返回了降级数据,然后需要 return nil

Go 源码中的Errors

1.13 版本

在基础库中,error的处理

最简单的错误检查:

image-20230728141034809

有时候需要对 sentinel error 进行检查

image-20230728141053425

实现了 error interface 的自定义 error struct ,进行断言获取更丰富的上下文

image-20230728141127533

函数在调用栈中添加信息向上传递错误,例如对错误发生时发生的情况的简要描述

image-20230728141200726

使用创建新错误 fmt.Errorf 丢弃原始错误中除文本外的所有内容。正如上面的 QueryError 中看到的那样,有时候可能需要定义一个包含底层错误的新错误类型,并将其保存以供代码检查。这里是 QueryError

image-20230728141332886

程序可以查看 QueryError 值,以根据底层错误做出决策

image-20230728141353916

这几种做法,都比较复杂。

go 1.13 为 errorsfmt 标准库引入了新特性,以简化处理包含其他错误的错误。其中最重要的是:包含另一个错误的error可以实现返回底层错误的 Unwrap 方法。如果 e1.Unwrap()返回e2,那么可以说e1包装e2,可以展开e1获得e2

按照此约定,可以为上面的QueryError类型指定一个Unwrap方法,返回包含的错误:

image-20230728141829124

Go 1.13 errors包包含两个用于检查错误的新函数,Is 和 As

image-20230728141902827

errors.Is 用于判断两个err是否相等,内部会自动调用Unwrap,获取最底层的error

源码:

go/src/errors/wrap.go

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
35
36
37
// Is reports whether any error in err's chain matches target.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
//
// An error type might provide an Is method so it can be treated as equivalent
// to an existing error. For example, if MyError defines
//
// func (m MyError) Is(target error) bool { return target == fs.ErrExist }
//
// then Is(MyError{}, fs.ErrExist) returns true. See syscall.Errno.Is for
// an example in the standard library. An Is method should only shallowly
// compare err and the target and not call Unwrap on either.
func Is(err, target error) bool {
if target == nil {
return err == target
}

isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// TODO: consider supporting target.Is(err). This would allow
// user-definable predicates, but also may allow for coping with sloppy
// APIs, thereby making it easier to get away with them.
if err = Unwrap(err); err == nil {
return false
}
}
}

image-20230728141907543

使用errors.As,判断err类型

源码:

go/src/errors/wrap.go

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
35
36
37
38
39
40
41
// As finds the first error in err's chain that matches target, and if one is found, sets
// target to that error value and returns true. Otherwise, it returns false.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method As(interface{}) bool such that
// As(target) returns true. In the latter case, the As method is responsible for
// setting target.
//
// An error type might provide an As method so it can be treated as if it were a
// different error type.
//
// As panics if target is not a non-nil pointer to either a type that implements
// error, or to any interface type.
func As(err error, target any) bool {
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
targetType := typ.Elem()
if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
for err != nil {
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
return true
}
err = Unwrap(err)
}
return false
}

使用%w

使用 fmt.Errorf向错误添加附加信息

image-20230728142509547

在Go 1.13中fmt.Errorf支持新的%w谓词

image-20230728142552414

%w包装错误可用于errors.Is以及errors.As

image-20230728142624583

实现也很简单

image-20230728142716707

思想跟pkg/errors相同,都是存储原始错误加上附加额外信息。

定制错误判断

Customizing error tests with Is and As methods

例如errors.Is源码,通过判断err类型、实现的方法,使用底层error进行判断

image-20230728142953312

使用源码的思想,自定义的error也可以实现相同的判断方式

image-20230728142959115

代码示例

避免出现无法添加额外信息的情况

image-20230728143449166

如果直接返回ErrPermission,判断错误的时候,如果增加了一些额外的信息,例如userInfo,则等值判断会失效。

但是如果返回fmt.Errorf封装后的err,使用errors.Is就可以判断出。

两种errors结合

errorsgithub.com/pkg/errors相结合。

1.13 errors包相比pkg的包,没有携带堆栈信息。原因是避免破坏API兼容性。

因此pkg中也兼容了errorsUnwrapIs方法

源码

github.com/pkg/errors/errors.go

1
2
// Unwrap provides compatibility for Go 1.13 error chains.
func (w *withStack) Unwrap() error { return w.error }

github.com/pkg/errors/go113.go

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
// Is reports whether any error in err's chain matches target.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error is considered to match a target if it is equal to that target or if
// it implements a method Is(error) bool such that Is(target) returns true.
func Is(err, target error) bool { return stderrors.Is(err, target) }

// As finds the first error in err's chain that matches target, and if so, sets
// target to that error value and returns true.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
//
// An error matches target if the error's concrete value is assignable to the value
// pointed to by target, or if the error has a method As(interface{}) bool such that
// As(target) returns true. In the latter case, the As method is responsible for
// setting target.
//
// As will panic if target is not a non-nil pointer to either a type that implements
// error, or to any interface type. As returns false if err is nil.
func As(err error, target interface{}) bool { return stderrors.As(err, target) }

// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error {
return stderrors.Unwrap(err)
}

用法

image-20230728144516206

可以看到,pkg中的errerrorserr是兼容的。

同时返回多个error

使用multierr

https://github.com/uber-go/multierr

Go 2 Error 展望

proposal地址:https://go.googlesource.com/proposal/+/master/design/29934-error-values.md

新增方法

image-20230728144858946

隐藏错误

image-20230728144931800

堆栈信息

image-20230728144941564

堆栈打印效果

image-20230728145012841

打印时Unwrap

image-20230728145057902

os包增加错误判断

image-20230728145128349

推荐阅读

Why Go gets exceptions right

Errors and Exceptions, redux

Error handling vs. exceptions redux

Why Go’s Error Handling is Awesome

Effective error handling in Go.

Error handling and Go

Error Handling In Go, Part I

Error Handling In Go, Part II

Don’t just check errors, handle them gracefully

Error handling in Upspin

Errors are values

Stack traces and the errors package

Design Philosophy On Logging

Go 1.13: xerrors

Working with Errors in Go 1.13

Error Handling in Go

Error Handling in Go 1.13