Go-知识error
- [1. 发展过程](#1. 发展过程)
- [2. error 接口](#2. error 接口)
- [3. 创建error](#3. 创建error)
-
- [3.1 errors.New()](#3.1 errors.New())
- [3.2 fmt.Errorf()](#3.2 fmt.Errorf())
- [3.3 性能对比](#3.3 性能对比)
- [4. 自定义error](#4. 自定义error)
- [5. 异常处理](#5. 异常处理)
-
- [5.1 检查error](#5.1 检查error)
- [5.2 传递error](#5.2 传递error)
- [6. 链式error](#6. 链式error)
- [7. fmt.Errorf](#7. fmt.Errorf)
-
- [7.1 fmt.Errorf 只能接受一个%w](#7.1 fmt.Errorf 只能接受一个%w)
- [7.2 %w 只匹配error参数](#7.2 %w 只匹配error参数)
- [8. errors.Unwrap](#8. errors.Unwrap)
- [9. errors.Is](#9. errors.Is)
- [10. errors.As](#10. errors.As)
- [11. 普通 error 升级 wrapError](#11. 普通 error 升级 wrapError)
- [12. 总结](#12. 总结)
1. 发展过程
在Go 1.13 之前长达10余年的时间里,标准库对error的支持非常有限,仅有errors.New和fmt.Errorf
两个函数用来构造error实例。然而Go语言仅提供了error的内置接口定义(type error interface),
这样开发者可以定义任何类型的error,并可以存储任意的内容。
在Go 1.13 之前,已经有很多开源项目试图扩展标准库的error以满足实际项目的需要,比如pkg/errors,该项目被大量应用于诸如
Kubernates这样的大型项目中。
Go 1.13 在保持对原有error兼容的前提下,提供了新的error类型,新的error类型在函数间传递时
可以保存原始的error信息,这类error称为链式error.
2. error 接口
error是一种内建的接口类型,内建意味着不需要import任何包就可以直接使用,使用起来就像int,string一样。
error接口只声明了一个Error()方法,任何实现了该方法的结构体都可以作为error来使用。
error的实例代表一种异常状态,Error()方法用于描述该异常状态,值为nil的error代表没有异常。
标准库errors包中的errorString就是实现error接口的一个例子:
errorString是errors包的私有类型,对外不可见,只能通过相应的公开接口才可以创建errorString实例。
3. 创建error
标准库提供了两种创建error的方法:
errors.New()
fmt.Errorf()
3.1 errors.New()
errors.New的实现很简单,构造一个errorString的实例便返回:
3.2 fmt.Errorf()
errors.Errorf单调地接收一个字符串参数来构造error,而实际场景中往往需要使用fmt.Sprintf()生成字符串,此时可以直接使用fmt.Errorf
fmt.Errorf只是对errors.New的简单封装。
3.3 性能对比
fmt.Errorf适用于需要格式化输出错误字符串的场景,如果不需要格式化字符串,建议直接使用errors.New
如下case:
Go
func BenchmarkFmt(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Errorf("test error")
}
}
和
Go
func BenchmarkFmt(b *testing.B) {
for i := 0; i < b.N; i++ {
errors.New("test error")
}
}
使用benchstat分析:
因为fmt.Errorf在生成格式化字符串时需要遍历所有字符,所以性能上有损失。
考虑另一种case,需要格式化字符串的场景:
Go
func BenchmarkFmt(b *testing.B) {
for i := 0; i < b.N; i++ {
fmt.Errorf("test error : %s", "test")
}
}
Go
func BenchmarkFmt(b *testing.B) {
for i := 0; i < b.N; i++ {
errors.New(fmt.Sprintf("test error : %s", "test"))
}
}
使用fmt.Sprintf+errors.New,格式化字符串也比较快。
上述仅代表在我的环境下某个场景的验证,不具有普遍意义。
4. 自定义error
任何实现了error接口的类型都可以称为error,比如标准库os中的PathError就是一个例子:
5. 异常处理
针对error而言,异常处理包括如何检查错误,如何传递错误。
5.1 检查error
最常见的检查error的方式是与nil值进行比较
有时也会与一些预定义的error进行比较
由于任何实现了error接口的类型均可以作为error来处理,所以往往也会使用类型断言来检查error:
5.2 传递error
在一个函数中收到一个error,往往需要附加一些上下文信息再把error继续向上层抛。
最常见的添加附加上下文信息的方法是使用emf.Errorf
Go
func makeErr(err error) error {
return fmt.Errorf("%s error : %s", "test", err)
}
这种方式抛出的error有一个糟糕的问题,那就是原error信息和附加的信息被柔和到一起了。
因为是新创建的error了,在外面处理的时候,就不能使用类型断言了。
为了解决底层异常丢失的问题,可以参考PathError
Go
type PathError struct {
Op string
Path string
Err error
}
在进行断言的时候,可以先断言外层error,在断言底层error
Go
if e,ok := err.(*os.PathError);ok && e.Err == os.ErrPermission {
fmt.Println("permission denied")
}
6. 链式error
在G0 1.13 以前,使用fmt.Errorf传递捕获的error并为error增加上下文信息时,原error将和上下文信息混杂在一起,这样便无法获取原始的error。
为此Go 1.13中引入了一套解决方案,链式error. error在函数间传递时,上下文信息像一个链表一样把各个层级的error连接起来。
wrapError看起来很像os.PathError
os.PathError通过os.PathError.Op和os.PathError.Path保存上下文信息,通过os.PathError.Err保存下层error.
而wrapError的msg成员则把原error和上下文保存到一起,通过err成员保存原始的error.
在Go 1.13 中,在fmt.Errorf中用 wrapError替换了errorString。
同时还额外实现了Unwrap()接口,用于返回原始的error.
7. fmt.Errorf
在Go 1.13 中,fmt.Errorf新增了格式动词 %w
(wrap) 用于生成 wrapError 实例,并且兼容原有格式动词。
Go
func Errorf(format string, a ...interface{}) error {
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a)
s := string(p.buf)
var err error
// 如果没有 %w 动词,那么生成基础的 error
if p.wrappedErr == nil {
err = errors.New(s)
} else {
// 如果有 %w 动词,那么生成 wrapError
err = &wrapError{s, p.wrappedErr}
}
p.free()
return err
}
在print.doPrintf 中,有对 %w 的解析
首先 error 是接口,所以被归类在 method 一类中
找到%
的位置
因为都不符合,所以走的是default 分支
在对传入的入参进行类型处理
因为error是interface,所以会走这个逻辑
在handlerMethod的时候,会对%w的动词做处理
总得来说,在fmt.Errorf("xx %v", err)
中,因为使用的是%v,所以生成的还是 errorString.
在fmt.Errorf("xx %w", err)
中,因为使用的是%w,生成的是 wrapError .
7.1 fmt.Errorf 只能接受一个%w
根据上面的分析,可知,一次只能接受一个%w,因为wrapError中只能保存一个原始error。
如果一次性传入多个,编译器会给出编译错误的提示。
在 1.20 版本中,增加了wrapErrors类型,将error从单个成员扩展成数组
在Errorf解析的时候,如果没有wrapError,那么是errorString,如果有一个wrapError,那么就是wrapError如果有大于1个的wrapError,则使用wrapErrors
7.2 %w 只匹配error参数
因为格式动词%w
被归类在interface中,所以如果不是interface的参数,那么会编译失败
那么如果是普通的interface呢
Go
type X interface {
}
type x struct{}
func TestFmt(t *testing.T) {
a := x{}
wr := fmt.Errorf("x %w %w", a, a)
fmt.Println(wr)
fmt.Println(errors.Unwrap(wr))
}
即使是interface 类型的,也因为没有实现Error()而编译失败。
另外需要注意的是,虽然wrapError实现了Unwrap接口,但是由于error接口仍然只定义了一个Error方法,所以使用fmt.Errorf生成的error,
不能直接调用自身的Unwrap接口获得原始error,而需要使用errors包中提供的Unwrap方法
8. errors.Unwrap
Unwrap方法用于获取原始error,fmt.Errorf则是用于包装原始error
如果err没有实现Unwrap函数,则不是wrapError,直接返回nil,否则调用Unwrap函数并返回。
对于自定义的error类型,在实现Error函数的基础上,需要额外实现Unwrap函数,可以升级成链式error。
比如os.PathError
这里面有个知识点,在go里面是鸭子类型,实际上error并没有增加任何接口,但是因为鸭子类型的特点
,在判断的时候,通过
u, ok := err.(interface {
Unwrap() error
})
来判断,某个结构体是否实现了接口,并且可以调用Unwrap方法。
实际上你在源码中搜索,是没有任何对于Unwrap的接口定义的。
9. errors.Is
errors.Is 用于检查特定的error链中是否包含指定的error值。
Go
func Is(err, target error) bool {
// 如果目标是nil,那么判断err是否等于nil
if target == nil {
return err == target
}
// 目标类型是可比较的
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
return true
}
// 如果 err 实现了 Is 接口,那么调用 err 的 Is 函数判断
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// 否则不断的调用 Unwrap 进行获取原始 error 进行比较,直到原始 error为空
if err = Unwrap(err); err == nil {
return false
}
}
}
errors.Is逐层拆解err并与参数target对比,如果发现相等则返回true,否则返回false。
对于自定义error类型来书哦,如果实现了自己的Is方法,则在比较时,会先调用自身实现的Is方法。
10. errors.As
在对error进行类型断言的时候,如果是链式error,那么类型断言将不再有效了。
除非使用Unwrap一层一层的拆解,并尝试类型断言。
在Go 1.13 中,errors.As 用于从一个error链中查找是否有指定的类型出现,如果有,
那么把error转换成该类型。
Go
func As(err error, target interface{}) bool {
if target == nil {
panic("errors: target cannot be nil")
}
// 使用反射获取类型信息
val := reflectlite.ValueOf(target)
typ := val.Type()
// 检查不符合的类型
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
// 检查是否是error
if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
// 循环 Unwrap 并尝试类型断言
for err != nil {
// 如果 err 是目标类型,那么将 err 写入 target
// 这也意味着,target 需要引用传递才能得到预期的效果
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
// 如果 err 实现了 As ,那么尝试使用 err 的 As
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
// 否则继续拆解,直到原始 error 为空
err = Unwrap(err)
}
return false
}
11. 普通 error 升级 wrapError
因为Go 的每个版本都严格尊许兼容性规则,所以对于老版本的Go 语言开发的程序,可以直接升级,不需要做任何工作适配。
并且行为逻辑依然符合预期。
如果需要从 普通的 error 升级到 wrapError,则需要做以下适配 :
- 创建error时,fmt.Errorf中的格式化动词从%v改为%w
- 等值(==)比较,使用errors.Is代替
- 类型断言使用errors.As代替
- 自定义类型额外实现Unwrap方法
- 自定义类型额外实现As方法(可选)
- 自定义类型额外实现Is方法(可选)
12. 总结
error 只是一个内建接口,只要实现了接口,就是 error类型的变量。
创建通过 errors.New 和 fmt.Errorf 创建,区别在于是否格式化字符串。
Go sdk 实现了errorString 和 wrapError 内部类型。
Go 1.13 针对 error 的优化主要是解决了 error 传递时丢失原 error 信息的问题,通过扩展 fmt.Errorf
来支持创建链式的 error ,并通过 errors.Unwrap 拆解获得原始 error
errors.Is 递归地拆解 error 并检查是否是指定的error值。
errors.As 递归地拆解 error 并检查是否是指定的 error 类型,如果是,则将 error 写入指定的变量中。