0%

go 库学习之 errors

Go 采用的是基于 错误值比较 的错误处理机制,即通过检查返回的 error 值来判断函数/方法是否执行成功,如果执行失败,也是通过该 error 值来携带具体的错误信息。相比于其他编程语言的 异常 错误处理机制,这种 值比较 机制更加简单,它要求在程序控制流程中显式关注和处理每个错误,这样的代码往往可读性更好,也符合 Go 追求简答的设计哲学。

标准库的 errors 包为 Go 的错误处理机制提供了基础支持,这篇文章将学习 errors 包的基本用法和原理。

error 接口

这种基于 值比较 的错误处理方式使得我们可以使用任意类型来表示错误信息,例如可以像 C 语言惯例那样使用 int 类型来表示错误:0 表示成功,非 0 表示错误,不同的错误值(通常称为错误码)表示不同错误类型。C 语言这种使用整型值来表示错误的方式虽然简单,但它毕竟不是语言标准,无法让不同代码遵守同一套约定。而且整型值能够携带的错误信息也很少。

为了能够在所有 Go 代码之间统一地表示错误,Go 语言定义了 error 接口类型,它的定义如下:

1
2
3
type error interface {
Error() string
}

当我们的 Go 代码需要接收、处理、返回错误值时,可以统一使用 error 接口类型来表示错误。另一方面,我们可以根据自己的需要定义具体的错误类型,只要这些数据类型实现了 error 接口,那么这些类型的值都可以用于表示 错误。这样既保证了错误处理在不同 Go 代码之间的一致性,又赋予了 具体实现 灵活定义错误类型的自由。

这其实也是 面向接口 编程思想的体现。

errors.New 函数

errors 包内部定义了 errorString 类型,用于实现 error 接口:

1
2
3
4
5
6
7
8
type errorString struct {
s string
}

// *errorString 实现了 Error 接口
func (e *errorString) Error() string {
return e.s
}

errorString 类型是个非导出类型,通过 errors.New 函数创建一个该类型的实例:

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

可以看到 errorString 的实现非常简单:

  • 当调用 errors.New(string) 创建 errorString 实例时,它保存下传入的错误字符串
  • 当通过 Error() 方法获取错误信息时,直接返回所保存的错误字符串

但是需要注意到,errors.New 函数每次都是返回一个新的 errorString 实例,因此当对错误值进行比较式,两个实例即使保存的错误字符串相同,它们也是不相同的:

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

import "errors"

var ErrNotExist = errors.New("not exist")

func f() error {
return errors.New("not exist")
}

func main() {
if e := f(); e == ErrNotExist {
println("exist error")
} else {
// not exist error
println("not exist error")
}
}

为了解决上述问题,一般的做法是提前通过 errros.New() 定义好各种错误,每次总是使用这些已经定义好错误实例值:

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

import "errors"

var ErrNotExist = errors.New("not exist")

func f() error {
return ErrNotExist
}

func main() {
if e := f(); e == ErrNotExist {
// exist error
println("exist error")
} else {
println("not exist error")
}
}

errors.Join 函数

errors.Join() 函数可以将多个错误实例包装(wrap)成一个,参数中的 nil 会被忽略,如果所有错误实例值都是 nil,则 Join 函数也返回 nil。

1
func Join(errs ...error) error

Join 函数返回的错误实例类型是非导出类型 joinError 类型,它是以切片的形式保存所有的错误实例,该类型定义如下:

1
2
3
type joinError struct {
errs []error
}

Join 函数的核心实现就是将参数中的所有非 nil 的 error 保存到 joinError 的 err 切片中,并返回 joinError 类型的实例。

joinError 类型实现了 Error 接口,其 Error() 方法就是调用 err 切片中每个对象的 Error() 方法,并以 \n 作为分隔符连接各个错误字符串,并返回连接后的结果。

错误包裹链

当通过 Join() 将一组错误包装成一个错误后,我们如何判断应该如何进行错误的 值比较 呢?例如假设通过 Join 函数将 A 和 B 包装成一个错误 AB,我们预期 AB 错误应该也属于 A 错误。这就涉及 Go 对错误链的支持了。

当代码调用层次比较深时,每一层可能都会构造自己的错误信息,同时我们又不希望丢失原有的错误上下文信息,这时就需要通过 错误包裹链 机制来提供 错误的包裹 功能了。错误包裹机制可以接受一个或一组错误,将其包裹成一个新的错误,并且可以从该新错误中提取原有错误信息。Join 函数、fmt.Errorf 的 %w 都提供错误包裹能力。

go 的 erros 包提供了 Is 和 As 函数来从 包裹错误 中检视、提取原始错误。这两个函数依赖的核心机制就是如下接口:

1
2
interface{ Unwrap() error }
interface{ Unwrap() []error }

errors.Is 函数

errors.Is 函数用于判断 err 错误是否和指定的 target 错误匹配,该函数会沿着 err 的包裹链,逐步 Unwrap(),判断其返回值是否和 target 相等。下面展示了 errors.Is 函数的实现:

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
func Is(err, target error) bool {
// 如果某一个值是 nil,直接判断即可
if err == nil || target == nil {
return err == target
}

// 判断 target 是否是可比较类型,例如 slice 就不是可比较类型
isComparable := reflectlite.TypeOf(target).Comparable()
return is(err, target, isComparable)
}

// is 函数用于判断 err 错误是否和指定的 target 错误匹配
// targetComparable 表示 target 自己是否是可比较的数据类型
func is(err, target error, targetComparable bool) bool {
for {
// 如果 target 是可比较类型,可以直接比较
if targetComparable && err == target {
return true
}
// 如果 err 自己实现了 Is() 方法,则首先尝试调用其自己实现的 x.Is 方法进行判断
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
switch x := err.(type) {
// 如果 err 实现了 Unwrap() error 方法
case interface{ Unwrap() error }:
// 使用 Unwrap() 返回后的值继续比较
err = x.Unwrap()
if err == nil {
return false
}
// 如果 err 实现了 Unwrap() []error 方法
case interface{ Unwrap() []error }:
// 则遍历 Unwrap() 返回的每个 err,进行深度遍历检查
for _, err := range x.Unwrap() {
// 对每个 err 继续调用 is 进行检查
if is(err, target, targetComparable) {
return true
}
}
return false
// 如果没有实现上述两个方法,则返回 false
default:
return false
}
}
}

可以看到,errors.Is 函数的核心逻辑是:

  • 如果 err 自己实现了 Is 方法,首先调用其自己的 Is 方法进行判断
  • 否则判断 err 是否实现了 Unwrap() error 方法或 Unwrap() []error 方法。如果实现了这两个接口,则会调用 Unwrap 方法并对其返回的 err 进行判断
  • 以上过程会不断递归进行,直至得到判断结果。如果无法继续判断下去,则返回 false

errors.As 函数

errors.As 函数 As 函数用于判断 err 错误是否可以当成 target 错误类型,如果是可以的话,会在 target 中保存该错误值并返回 true,否则返回 false。直接。即 errors.Is() 函数用于是错误值的比较,而 errors.As() 则是错误类型的比较。

1
func As(err error, target any) bool

As 函数的实现如下:

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
 As(err error, target any) bool {
if err == nil {
return false
}
if target == nil {
panic("errors: target cannot be nil")
}
// 通过反射获取 target 的类型
val := reflectlite.ValueOf(target)
typ := val.Type()
// target 的类型必须是指针类型 或者是值为 nill 的指针类型
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
// 获取指针所指向值的数据类型
targetType := typ.Elem()
// targetType 类型必须是接口类型,且该接口类型必须实现了 Error 接口
if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
return as(err, target, val, targetType)
}

func as(err error, target any, targetVal reflectlite.Value, targetType reflectlite.Type) bool {
for {
// 判断 err 类型的值是否可以赋值给 targetType
if reflectlite.TypeOf(err).AssignableTo(targetType) {
// 如果可以,则完成值的设置
targetVal.Elem().Set(reflectlite.ValueOf(err))
return true
}
// 如果 err 自己实现了 As 方法,则调用 err 自己的 As 方法
if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
return true
}
switch x := err.(type) {
// 如果 err 实现了 Unwrap() error 方法
case interface{ Unwrap() error }:
// 使用 Unwrap 后的值进行判断
err = x.Unwrap()
if err == nil {
return false
}
// 如果 err 实现了 Unwrap() []error 方法
case interface{ Unwrap() []error }:
// 则遍历 Unwrap() 返回的每个 err,进行深度遍历检查
for _, err := range x.Unwrap() {
if err == nil {
continue
}
// 对每个 err 继续调用 as 进行检查
if as(err, target, targetVal, targetType) {
return true
}
}
return false
// 如果没有实现上述两个方法,则返回 false
default:
return false
}
}
}

// Error 接口类型
var errorType = reflectlite.TypeOf((*error)(nil)).Elem()

可以看到,errors.As 函数的逻辑类似于 errors.Is 函数:

  • 通过反射中的 reflectlite.Type.AssignableTo 方法判断 err 值是否可以赋值给 targetType
  • 如果 err 自己实现了 As 方法,首先调用其自己的 As 方法进行判断
  • 否则判断 err 是否实现了 Unwrap() error 方法或 Unwrap() []error 方法。如果实现了这两个接口,则会调用 Unwrap 方法并对其返回的 err 进行判断
  • 以上过程会不断递归进行,直至得到判断结果。如果无法继续判断下去,则返回 false

这里需要注意下 errors.As 函数的 target 必须是个非 nil 指针,且指针所指向类型必须是个接口类型或者实现了 Error() 方法的数据类型。

errors.Unwrap 函数

除了以上两个函数,errors 包还提供了 Unwrap 函数,可以直接尝试获取 err 所包裹的下一层错误。其实现非常简单:如果 err 实现了 Unwrap() error 方法,则直接调用该方法:

1
2
3
4
5
6
7
8
9
10
11
func Unwrap(err error) error {
// 如果实现了 Unwrap 方法,那么还会调用其 Unwrap 方法
u, ok := err.(interface {
Unwrap() error
})
// 没有实现 Unwrap 方法,直接返回 nil
if !ok {
return nil
}
return u.Unwrap()
}

特别注意,Unwrap 方法只判断了 err 是否实现了 Unwrap() error 方法,其并不会尝试调用 Unwrap() []error 方法。

简单示例

首先如下展示了 Join 函数的一个基本使用:

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

import (
"errors"
"fmt"
)

func main() {
errA := errors.New("A")
errB := errors.New("B")

// A\nB
errAB := errors.Join(errA, errB)
fmt.Println(errAB.Error())

// true
fmt.Println(errors.Is(errAB, errA))
}

可以看到 Join 函数也能够支持 错误包裹链,这正是因为其实现了 Unwrap() []error 方法

1
2
3
func (e *joinError) Unwrap() []error {
return e.errs
}

再来看一个自定义类型实现 错误包裹链 的例子:

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
package main

import (
"errors"
"fmt"
)

type wrappedError struct {
err error
msg string
}

type customizedError struct {
msg string
}

func (c *customizedError) Error() string {
return c.msg
}

func (w *wrappedError) Error() string {
return w.msg
}

func (w *wrappedError) Unwrap() error {
return w.err
}

func main() {
err1 := errors.New("1")
err2 := errors.New("2")
cErr1 := &customizedError{"customized error 1"}
cErr2 := &customizedError{"customized error 2"}

wrap1 := &wrappedError{
msg: "wrap1",
err: err1,
}

wrapc := &wrappedError{
msg: "wrap customized",
err: cErr1,
}

// true
fmt.Println(errors.Is(wrap1, err1))
// false
fmt.Println(errors.Is(wrap1, err2))

if errors.As(wrap1, &err2) {
// // wrap1
fmt.Println(err2.Error())
}

// true
fmt.Println(errors.Is(wrapc, cErr1))
// false
fmt.Println(errors.Is(wrapc, cErr2))

// customized error 2
fmt.Println(cErr2.Error())
if errors.As(wrapc, &cErr2) {
// customized error 1
fmt.Println(cErr2.Error())
}

}

这个例子中,需要特别注意的是 errors.New() 函数的返回的是 error 接口类型,因此当调用 errors.As(wrap1, &err2) 时,其实是判断 wrap1 是否可以赋值给 error 接口类型,因此 err2 最后其实保存的是值其实是 wrap1 本身。

小结

这篇文章学习了 go 库 erros 包所提供的基础设施,包括 errors.New 函数、errors.Join 函数等。同时 errors 包通过 Unwrap 接口实现了对 错误包裹链 的支持,我们可以通过 errors.Unwrap、errors.Is、errors.As 函数来对错误链中的错误进行提取和检视。