除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态。error 接口是 Go 原生内置的类型,它的定义如下:
// $GOROOT/src/builtin/builtin.go
type interface error {
Error() string
}
在这个接口类型的声明中只包含了一个方法 Error 。Error 方法不接受任何参数,但是会返回一个 string 类型的结果。它的作用是返回错误信息的字符串表示形式。
任何实现了 error 的 Error 方法的类型的实例,都可以作为错误值赋值给 error 接口变量。
一般情况下在 Go 里只使用 error 类型判断错误, Go 官方希望开发者能够很清楚的掌控所有的异常,在每一个可能出现异常的地方都返回或判断 error 是否存在。
标准库 errors.New 和 fmt.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 22.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 创建包装错误变量 err1 和 err2,其中 err1 实现了对 ErrSentinel 这个“哨兵错误值”的包装,而 err2 又对 err1 进行了包装,这样就形成了一条错误链。位于错误链最上层的是 err2,位于最底层的是 ErrSentinel。之后,我们再分别通过值比较和 errors.Is这两种方法,判断 err2 与 ErrSentinel 的关系。运行上述代码,我们会看到如下结果:
false err2 is ErrSentinel
我们看到,通过比较操作符对 err2 与 ErrSentinel 进行比较后,我们发现这二者并不相同。而 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 方法去检视某个错误值是否是某自定义错误类型的实例。



