起步
如果说 Go 有很多诟病的地方,那么 Go 中 error 的处理一定可以挤进吐槽榜单前十。既然 try
语句提议被一拒再拒,我们也只好用着古老的 if 筛选错误。Go 官方并非没有意识到 error 的鸡肋问题,于是在 Go 1.13 提出了新解决方案,总的说来就是“三个 api + 一个格式占位符”。
error 从何而来
在 Go 中,error 从何而来呢?熟练使用 Go 的人一定知道以下几种方式:
- errors.New
- fmt.Errorf
- 直接返回函数调用后得到的 error
- 定义一个结构体,实现 error 接口(
type error Interface { Error() string }
)
以“打开一个文件”为例,按照上面的处理方式代码可以分别是:
// errors.New
func openConfigFile(path string) error {
_, err := os.Open(path)
if err != nil {
// 返回新的 error 实例
return errors.New("can not open the specific file")
}
return nil
}
// fmt.Errorf
func openConfigFile(path string) error {
_, err := os.Open(path)
if err != nil {
// 返回新的 error 实例,同时包含了实际 error
return fmt.Errorf("can not open the specific file, reason: %v", err)
}
return nil
}
// return error that called function returned
func openConfigFile(path string) error {
_, err := os.Open(path)
if err != nil {
// 直接返回得到的 error,不做任何处理
return err
}
return nil
}
// 自定义 error
type OpenErr struct {
Err error // 存放原始 error
}
// 实现 error 接口
func (*OpenErr) Error() string {
return "can not open the specific file"
}
func openConfigFile(path string) error {
_, err := os.Open(path)
if err != nil {
return &OpenErr{Err:err}
}
return nil
}
上述四种方法各有千秋。如方法一,errors.New 会返回一个新的 error,其存放的数据就是我们传入的 text(“can not open the specific file”)。采用这种方式通常是为了告诉调用者出错了,但实际的错误细节不愿暴露。对调用者来说,他可能不太关心出了什么错,只在乎有没有出错。
方法二跟方法一相同,也会隐藏原始错误(这里指错误类型),但通常会将原始错误的字符串说明一起返回。在该处理方式中,“can not open the specific file” 为额外提示语,“reason: %v” 显示错误细节。一般来讲,调用 fmt.Errorf 更大几率是要产生一个给人看的错误,而不是让代码去解析(尽管并非不能)。
方法三,通常是函数调用者需要根据 error 的实际类型,或实际值,确定下一步的执行策略。也就是说它需要解析 error,所以函数返回“原味” error。这样做的缺点是,没办法对 error 添加额外信息。
方法四可以认为是上述三种方法的集合,既可以保留原始 error,还可以添加额外信息。通过这些元数据随意组合,调用者想要的样子它都有。缺点就是代码要多写一些。
如何检查 error
现在 error 有了,我们应该如何检查错误呢?在 1.13 之前,常见有:1. 比较值;2. 比较类型。
拿官方源码举例更具说服力。我们先来看看比较值。
func (db *DB) QueryContext(ctx context.Context,
query string,
args ...interface{}) (*Rows, error) {
var rows *Rows
var err error
for i := 0; i < maxBadConnRetries; i++ {
rows, err = db.query(ctx, query, args, cachedOrNewConn)
if err != driver.ErrBadConn { // 比较值
break
}
}
if err == driver.ErrBadConn { // 比较值
return db.query(ctx, query, args, alwaysNewConn)
}
return rows, err
}
func (db *DB) Query(query string,
args ...interface{}) (*Rows, error) {
return db.QueryContext(context.Background(), query, args...)
}
var ErrBadConn = errors.New("driver: bad connection")
上述是 sql.Query
的源码,确定处理 ErrBadConn 错误,就是通过比较值实现的。
比较类型处理 error 在 Go 源码中更常用,主要是这种处理方式更灵活,错误信息更丰富。
我们先写一个 demo,代码的主要目的是访问某网站。访问过程中可能出现各种异常,而我们只处理超时异常。
// http 客户端
client := &http.Client{
Timeout: 3 * time.Second, // 3 秒超时
}
// 请求不存在的 url
_, err := client.Get("http://www.meiyuouzhegewangzhan.com")
if err != nil {
if os.IsTimeout(err) { // 超时 err
fmt.Println("timeout")
} else { // 其他 err
fmt.Println("other errors")
}
}
从上得知,判断一个 error 是否是超时异常,需要调用 os.IsTimeout
,源码如下。
func IsTimeout(err error) bool {
// 类型断言
terr, ok := underlyingError(err).(timeout)
return ok && terr.Timeout()
}
func underlyingError(err error) error {
// 返回实际 err
switch err := err.(type) {
case *PathError:
return err.Err
case *LinkError:
return err.Err
case *SyscallError:
return err.Err
}
// 如果没有潜在 err, 返回自身
return err
}
不论是 IsTimeout
还是 underlyingError
都借助类型比较实现对不同错误类型做处理。另外,继续跟进 PathError、LinkError、SysCallError 你会发现,这类 error 都是之前提到的第四种方法。它们包装了原始 error,但又会在一定场合下把它取出来(如上述代码中的 err.Err
)。
1.13+ 如何检查 error
不得不承认,过去检查 error 的方式有点麻烦,尤其是在 error 被包装过多时。假设一个 error 被包装了 3 层,此时又需要检查最里层的 error,意味着代码需要这样写:
e1, _ := e.(Err1)
e2, _ := e1.(Err2)
e3, _ := e2.(Err3)
if e3 == targetErr {
// handle
}
为了避免这种麻烦,1.13 开始,Go 提供了两个方法,用于检查链式中的 error。分别是比较值的 errors.Is
,以及比较类型的 errors.As
。想支持 error 链的检查,要求结构体(或其他错误类型)实现匿名接口:interface { Unwrap() error }
。
Unwrap
Unwrap 接口很好理解,用于返回结构体中的原始 error。拿 PathError 的源码举例:
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Unwrap() error { return e.Err }
errors.Is
errors.Is 通过值比较确定错误,这里有易错点。由于许多 error 是指针类型,而指针类型的比较是:比较指针变量存放的地址是否相同。用代码解释更容易理解:
err1 := errors.New("error")
err2 := errors.New("error")
// 指针比较
fmt.Println(err1 == err2) // false
// 值比较
err1Elem := reflect.ValueOf(err1).Elem().Interface()
err2Elem := reflect.ValueOf(err2).Elem().Interface()
fmt.Println(err1Elem == err2Elem) // true
err1 与 err2 本质上可以认为是一个 error,连错误信息都相同,但直接比较会得到 false。这是因为 err1 与 err2 有不同的地址。第二种方式则是将两个指针指向的结构体取出来,比较两个结构体值,从而获得 true 的结果。
官方包对这类问题的处理常常是,定义一组错误变量,在之后的整个程序运行期间都使用已经赋值的错误变量——确保同一类错误的指针总是指向同一个地址。
// go 源码
var (
// ErrInvalid indicates an invalid argument.
// Methods on File will return this error when the receiver is nil.
ErrInvalid = errInvalid() // "invalid argument"
ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
ErrNoDeadline = errNoDeadline() // "file type does not support deadline"
)
func errInvalid() error { return oserror.ErrInvalid }
func errPermission() error { return oserror.ErrPermission }
func errExist() error { return oserror.ErrExist }
func errNotExist() error { return oserror.ErrNotExist }
func errClosed() error { return oserror.ErrClosed }
func errNoDeadline() error { return poll.ErrNoDeadline }
判断文件是否存在的两种方式:
var err error
f, err := os.Open("不存在文件")
defer f.Close()
// 方法1
if os.IsNotExist(err) { // 进入 if stmt
fmt.Println("文件不存在")
}
// 方法2
if errors.Is(err, os.ErrNotExist) { // 进入 if stmt
fmt.Println("文件不存在")
}
所以根据以上特性,errors.Is 几乎不能处理 error 链。聪明的官方库怎么会想不到这点,解决方案要从源码找:
// go 源码
func Is(err, target error) bool {
...
isComparable := reflectlite.TypeOf(target).Comparable()
for {
// 值比较
if isComparable && err == target {
return true
}
// 调用 Is 比较
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
...
if err = Unwrap(err); err == nil {
return false
}
}
}
errors.Is 会先进行值比较,如果失败,那就有 Is 调用 Is,所以我们可以给自定义的 error 实现 Is 方法,用于填写 error 是否相等的处理逻辑。
// Err1
type Err1 struct {
Err error
}
func (e *Err1) Error() string { return "err1" }
func (e *Err1) Unwrap() error { return e.Err }
func (e * Err1) Is(other error) bool {
v1 := reflect.ValueOf(e)
v2 := reflect.ValueOf(other)
// 如果是空指针直接返回
if v1.IsNil() || v2.IsNil() {
return false
}
// 取出指针指向的变量
v1 = v1.Elem()
if v2.Kind() == reflect.Ptr {
v2 = v2.Elem()
}
return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}
// Err2
type Err2 struct {
Err error
}
func (e *Err2) Error() string { return "err2" }
func (e *Err2) Unwrap() error { return e.Err }
func (e *Err2) Is(other error) bool {
v1 := reflect.ValueOf(e)
v2 := reflect.ValueOf(other)
// 如果是空指针直接返回
if v1.IsNil() || v2.IsNil() {
return false
}
// 取出指针指向的变量
v1 = v1.Elem()
if v2.Kind() == reflect.Ptr {
v2 = v2.Elem()
}
return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}
// Err3
type Err3 struct {
Err error
}
func (e *Err3) Error() string { return "err3" }
func (e *Err3) Unwrap() error { return e.Err }
func (e *Err3) Is(other error) bool {
v1 := reflect.ValueOf(e)
v2 := reflect.ValueOf(other)
// 如果是空指针直接返回
if v1.IsNil() || v2.IsNil() {
return false
}
// 取出指针指向的变量
v1 = v1.Elem()
if v2.Kind() == reflect.Ptr {
v2 = v2.Elem()
}
return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}
// 产生 error
func genErr() error {
return &Err1{
Err: &Err2{
Err: &Err3{
Err: nil,
},
},
}
}
func main() {
err := genErr()
err3 := &Err3{Err:nil}
fmt.Println(errors.Is(err, err3))
}
代码明显累赘起来,往下看,还有更优雅的办法。
errors.As
当存在错误链时,我们更倾向于用类型定位错误,使用 errors.As 而不是 errors.Is。这样做也能避免冗余的 Is 方法。
// Err1
type Err1 struct {
Err error
}
func (e *Err1) Error() string { return "err1" }
func (e *Err1) Unwrap() error { return e.Err }
// Err2
type Err2 struct {
Err error
}
func (e *Err2) Error() string { return "err2" }
func (e *Err2) Unwrap() error { return e.Err }
// Err3
type Err3 struct {
Err error
}
func (e *Err3) Error() string { return "err3" }
func (e *Err3) Unwrap() error { return e.Err }
// 产生 error
func genErr() error {
return &Err1{
Err: &Err2{
Err: &Err3{
Err: nil,
},
},
}
}
func main() {
err := genErr()
var err3 *Err3
fmt.Println(errors.As(err, &err3)) // 第二个参数要求是 指针的地址
}
占位符 %w
大多数情况下我们用不着大动干戈的自定义错误类型,而喜欢采用第二种方式 fmt.Errorf
。从 1.13 开始,使用 %w 占位符会把原始 error 包裹起来放到 err 字段里,并构建一个新的 error 字符串放到 msg 中,对应的结构体就是 wrapError:
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
也就是说,以下用法不会丢失原始 error,反而会构建 error 链,方便 errors.Is 与 errors.As 进行错误检查。
if _, err := os.Open("xxx.txt") {
return fmt.Errorf("failed open, reason: %w", err)
}
还不快抢沙发