1. error 接口定义
除用 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 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
创建包装错误变量 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
方法去检视某个错误值是否是某自定义错误类型的实例。