Go 标准库学习:errors
原生 error
在 Go 的错误处理中,下面的代码占绝大多数:
1
2
3
4
if err != nil {
//....
return err
}
在满足业务需求的情况下,这种错误处理其实是最推荐的方式,这种直接透传的方式让代码之间的耦合度更低。在很多情况下,如果不关心错误中的具体信息,使用这种方式就可以了。
提前定义好 error
原生的 error
在有些情况下使用起来就不是很方便,比如我需要获得具体的错误信息,如果还用上面的方式来使用 error
,可能会出现下面的代码:
1
2
3
if err != nil && err.Error() == "invalid param" {
//...
}
写过代码的都知道上面的代码很不优雅,一方面,使用了魔法值,另外如果错误的信息变化之后,这里的代码逻辑就会出错。
可以通过把错误定义成一个变量:
1
2
3
var (
ErrInvalidParam = errors.New("invalid param")
)
那么上面的代码就可以变成这样:
1
2
3
if err != nil && err == ErrInvalidParam {
//...
}
如果一次性需要处理的错误比较多,还可以使用 switch
进行处理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if err != nil {
switch err {
case ErrInvalidParam:
//..
return
case ErrNetWork:
//...
return
case ErrFileNotExist:
//..
return
default:
//...
return
}
}
但是这种方式还不完美,因为 error
在传递的过程中,有可能会被包装,以携带更多的堆栈信息,比如下面这样:
1
2
3
4
if err != nil {
// 在包装错误的时候,这里格式化错误要使用 %w
return fmt.Errorf("add error info: %+v, origin error: %w", "other info", err)
}
假设上面被包装的错误是 ErrInvalidParam
,那么在调用的地方判断错误,就不能使用下面的代码:
1
2
3
if err != nil && err == ErrInvalidParam {
//...
}
为了解决这个问题, errors.Is
函数可以判断被包装的 error
中是否有预期的 error:
1
2
3
if errors.Is(err, ErrInvalidParam) {
//..
}
尽量使用 errors.Is
来替代对 error
的比较。
使用自定义的错误类型
上面的 error 使用方式在某些情况下还是不能满足要求。假如对于上面的无效参数 error
,业务方想要知道具体是哪个参数无效,直接定义的错误就无法满足要求。
error
本质是一个接口,也就是是说,只要实现了 Error
方法,就是一个 error
类型:
1
2
3
type error interface {
Error() string
}
那么就可以自定义一种错误类型:
1
2
3
4
5
6
7
8
type ErrInvalidParam struct {
ParamName string
ParamValue string
}
func (e *ErrInvalidParam) Error() string {
return fmt.Sprintf("invalid param: %+v, value: %+v", e.ParamName, e.ParamValue)
}
然后就可以使用类型断言机制或者类型选择机制,来对不同类型的错误进行处理:
1
2
3
4
e, ok := err.(*ErrInvalidParam)
if ok && e != nil {
//...
}
同样可以在 switch
中使用:
1
2
3
4
5
6
7
8
9
10
if err != nil {
switch err.(type) {
case *ErrInvalidParam:
//..
return
default:
//...
return
}
}
在这里 error
同样会存在被包装的问题,而 errors.As
刚好可以用来解决这个问题,可以判断出被包装的错误中是否存在某个 error
类型:
1
2
3
4
var e *ErrInvalidParam
if errors.As(err, &e) {
//..
}
更灵活的 error 类型
上面的方式已经可以解决大部分场景的 error
处理了,但是在一些复杂的情况下,可能需要从错误中获取更多的信息,还包含一定的逻辑处理。
在 Go 的 net 包中,有这样的一个接口:
1
2
3
4
5
type Error interface {
error
Timeout() bool
Temporary() bool
}
在这个接口中,有两个方法,这两个方法会对这个错误类型进行处理,判断是超时错误还是临时错误,实现了这个接口的 error
要实现这两个 方法,实现具体的判断逻辑。
在处理具体 error
时,会调用相应的方法来判断:
1
2
3
4
5
6
if ne, ok := e.(net.Error); ok && ne.Temporary() {
// 对临时错误进行处理
}
if ne, ok := e.(net.Error); ok && ne.Timeout() {
// 对超时错误进行处理
}
这种类型的 error
相对来说,使用的会比较少,一般情况下,尽量不要使用这么复杂的处理方式。
errors 中的其他能力
在 errors
包中,除了上面提到的 errors.Is
和 errors.As
两个很有用的函数之外,还有一个比较实用的函数 errors.Unwrap
。这个函数可以从包装的错误中将原错误解析出来。
可以使用 fmt.Errorf
来包装 error
,需要使用 %w 的格式化:
1
return fmt.Errorf("add error info: %+v, origin error: %w", "other info", err)
在后续的 error
处理时,可以调用 errors.Unwrap
函数来获得被包装前的 error
:
1
2
err = errors.Unwrap(err)
fmt.Printf("origin error: %+v\n", err)