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 保存到 joinErrorerr 切片中,并返回 joinError 类型的实例。

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

错误包裹链

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

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

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

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.Unwraperrors.Iserrors.As 函数来对错误链中的错误进行提取和检视。