Go 学习笔记(64)— Go error.New 创建接口错误对象、fmt.Errorf 创建接口错误对象、errors.Is 和 errors.As

1. error 接口定义

除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。error 接口是 Go 原生内置的类型,它的定义如下:

// $GOROOT/src/builtin/builtin.go
type interface error {
    Error() string
}

在这个接口类型的声明中只包含了一个方法 ErrorError 方法不接受任何参数,但是会返回一个 string 类型的结果。它的作用是返回错误信息的字符串表示形式。

任何实现了 errorError 方法的类型的实例,都可以作为错误值赋值给 error 接口变量。

一般情况下在 Go 里只使用 error 类型判断错误, Go 官方希望开发者能够很清楚的掌控所有的异常,在每一个可能出现异常的地方都返回或判断 error 是否存在。

标准库 errors.Newfmt.Errorf 函数用于创建实现 error 接口的错误对象。通过判断错误对象实例来确定具体错误类型。

err := errors.New("your first demo error")
errWithCtx = fmt.Errorf("index %d is out of bounds", i)

这两种方法实际上返回的是同一个实现了 error 接口的类型的实例,这个未导出的类型就是errors.errorString,它的定义是这样的:

// $GOROOT/src/errors/errors.go
type errorString struct {
    s string
}

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

2. 创建 error 接口对象

2.1 errors.New 创建 error 接口错误对象

在生成 error 类型值的时候,用到了 errors.New 函数。

这是一种最基本的生成错误值的方式。我们调用它的时候传入一个由字符串代表的错误信息,它会给返回给我们一个包含了这个错误信息的 error 类型值。

该值的静态类型当然是 error ,而动态类型则是一个在 errors 包中的,包级私有的类型 *errorString

package main

import "errors"

var ErrDivByZero = errors.New("division by zero")

func div(x, y int) (int, error) {
	if y == 0 {
		return 0, ErrDivByZero
	}
	return x / y, nil
}
func main() {
	switch z, err := div(10, 0); err {
	case nil:
		println(z)
	case ErrDivByZero:
		panic(err)
	}
}

输出结果:

panic: division by zero

goroutine 1 [running]:
main.main()
	/home/wohu/gocode/src/hello.go:18 +0xa7
exit status 2

2.2 fmt.Errorf 创建 error 接口错误对象

我们已经知道,通过调用 fmt.Printf 函数,并给定占位符 %s 就可以打印出某个值的字符串表示形式。对于其他类型的值来说,只要我们能为这个类型编写一个 String 方法,就可以自定义它的字符串表示形式。

而对于 error 类型值,它的字符串表示形式则取决于它的 Error 方法。在上述情况下,fmt.Printf 函数如果发现被打印的值是一个 error 类型的值,那么就会去调用它的 Error 方法。fmt 包中的这类打印函数其实都是这么做的。

顺便提一句,当我们想通过模板化的方式生成错误信息,并得到错误值时,可以使用 fmt.Errorf 函数。该函数所做的其实就是先调用 fmt.Sprintf 函数,得到确切的错误信息;再调用 errors.New 函数,得到包含该错误信息的 error 类型值,最后返回该值。

package main

import (
	"errors"
	"fmt"
)

func main() {
	err1 := fmt.Errorf("invalid contents: %s", "#$%")
	err2 := errors.New(fmt.Sprintf("invalid contents: %s", "#$%"))
	if err1.Error() == err2.Error() {
		fmt.Println("The error messages in err1 and err2 are the same.")
	}
}

输出结果:

The error messages in err1 and err2 are the same.

fmt.Errorf 函数 示例:

package main

import "fmt"

func foo(i int, j int) (r int, err error) {
    if j == 0 {
        err = fmt.Errorf("参数 2 不能为 %d", j) //给 err 变量赋值一个 error 对象
        return //返回 r 和 err,因为定义了返回值变量名,所以不需要在这里写返回变量
    }

    return i / j, err //如果没有赋值 error 给 err 变量,err 是 nil
}

func main() {
    //传递 add 函数和两个数字,计算相加结果
    n, err := foo(100, 0)
    if err != nil { //判断返回的 err 变量是否为 nil,如果不是,说明函数调用出错,打印错误内容
        println(err.Error())
    } else {
        println(n)
    }
}

3. 可导出哨兵错误值方式

Go 标准库采用了定义导出的“哨兵”错误值的方式,来辅助错误处理方检视错误值并做出错误处理分支的决策,比如下面的 bufio 包中定义的“哨兵错误”:

// $GOROOT/src/bufio/bufio.go
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")
)

下面的代码片段利用了上面的哨兵错误,进行错误处理分支的决策:

data, err := b.Peek(1)
if err != nil {
    switch err {
    case bufio.ErrNegativeCount:
        // ... ...
        return
    case bufio.ErrBufferFull:
        // ... ...
        return
    case bufio.ErrInvalidUnreadByte:
        // ... ...
        return
    default:
        // ... ...
        return
    }
}

一般“哨兵”错误值变量以 ErrXXX 格式命名。
Go 1.13 版本开始,标准库 errors 包提供了 Is 函数用于错误处理方对错误值的检视。Is 函数类似于把一个 error 类型变量与“哨兵”错误值进行比较,比如下面代码:

// 类似 if err == ErrOutOfBounds{ … }
if errors.Is(err, ErrOutOfBounds) {
    // 越界的错误处理
}

不同的是,如果 error 类型变量的底层错误值是一个包装错误(Wrapped Error),errors.Is 方法会沿着该包装错误所在错误链(Error Chain),与链上所有被包装的错误(Wrapped Error)进行比较,直至找到一个匹配的错误为止。下面是 Is 函数应用的一个例子:

var ErrSentinel = errors.New("the underlying sentinel error")

func main() {
  err1 := fmt.Errorf("wrap sentinel: %w", ErrSentinel)
  err2 := fmt.Errorf("wrap err1: %w", err1)
  println(err2 == ErrSentinel) //false
  if errors.Is(err2, ErrSentinel) {
    println("err2 is ErrSentinel")
    return
  }

  println("err2 is not ErrSentinel")
}

在这个例子中,我们通过 fmt.Errorf 函数,并且使用 %w 创建包装错误变量 err1err2,其中 err1 实现了对 ErrSentinel 这个“哨兵错误值”的包装,而 err2 又对 err1 进行了包装,这样就形成了一条错误链。位于错误链最上层的是 err2,位于最底层的是 ErrSentinel。之后,我们再分别通过值比较和 errors.Is这两种方法,判断 err2ErrSentinel 的关系。运行上述代码,我们会看到如下结果:

false
err2 is ErrSentinel

我们看到,通过比较操作符对 err2ErrSentinel 进行比较后,我们发现这二者并不相同。而 errors.Is 函数则会沿着 err2 所在错误链,向下找到被包装到最底层的“哨兵”错误值 ErrSentinel

所以,如果你使用的是 Go 1.13 及后续版本,我建议你尽量使用 errors.Is 方法去检视某个错误值是否就是某个预期错误值,或者包装了某个特定的“哨兵”错误值。

4. 错误值类型检视策略

上面基于 Go 标准库提供的错误值构造方法构造的“哨兵”错误值,除了让错误处理方可以“有的放矢”的进行值比较之外,并没有提供其他有效的错误上下文信息。那如果遇到错误处理方需要错误值提供更多的“错误上下文”的情况,上面这些错误处理策略和错误值构造方式都无法满足。

这种情况下,我们需要通过自定义错误类型的构造错误值的方式,来提供更多的“错误上下文”信息。并且,由于错误值都通过 error 接口变量统一呈现,要得到底层错误类型携带的错误上下文信息,错误处理方需要使用 Go 提供的类型断言机制(Type Assertion)或类型选择机制(Type Switch),这种错误处理方式,我称之为错误值类型检视策略。

Go 1.13 版本开始,标准库 errors 包提供了As 函数给错误处理方检视错误值。As 函数类似于通过类型断言判断一个 error 类型变量是否为特定的自定义错误类型,如下面代码所示:

// 类似 if e, ok := err.(*MyError); ok { … }
var e *MyError
if errors.As(err, &e) {
    // 如果err类型为*MyError,变量e将被设置为对应的错误值
}

不同的是,如果 error 类型变量的动态错误值是一个包装错误,errors.As 函数会沿着该包装错误所在错误链,与链上所有被包装的错误的类型进行比较,直至找到一个匹配的错误类型,就像 errors.Is 函数那样。下面是 As 函数应用的一个例子:

type MyError struct {
    e string
}

func (e *MyError) Error() string {
    return e.e
}

func main() {
    var err = &MyError{"MyError error demo"}
    err1 := fmt.Errorf("wrap err: %w", err)
    err2 := fmt.Errorf("wrap err1: %w", err1)
    var e *MyError
    if errors.As(err2, &e) {
        println("MyError is on the chain of err2")
        println(e == err)                  
        return                             
    }                                      
    println("MyError is not on the chain of err2")
} 

运行上述代码会得到:

MyError is on the chain of err2
true

我们看到,errors.As 函数沿着 err2 所在错误链向下找到了被包装到最深处的错误值,并将 err2 与其类型 * MyError 成功匹配。匹配成功后,errors.As 会将匹配到的错误值存储到 As 函数的第二个参数中,这也是为什么 println(e == err) 输出 true 的原因。

所以,如果你使用的是 Go 1.13 及后续版本,请尽量使用 errors.As 方法去检视某个错误值是否是某自定义错误类型的实例。

  • 10
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值