Go-知识error

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:

func BenchmarkFmt(b *testing.B) {
	for i := 0; i < b.N; i++ {
		fmt.Errorf("test error")
	}
}

func BenchmarkFmt(b *testing.B) {
	for i := 0; i < b.N; i++ {
		errors.New("test error")
	}
}

使用benchstat分析:
在这里插入图片描述

因为fmt.Errorf在生成格式化字符串时需要遍历所有字符,所以性能上有损失。
考虑另一种case,需要格式化字符串的场景:

func BenchmarkFmt(b *testing.B) {
	for i := 0; i < b.N; i++ {
		fmt.Errorf("test error : %s", "test")
	}
}
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

func makeErr(err error) error {
	return fmt.Errorf("%s error : %s", "test", err)
}

这种方式抛出的error有一个糟糕的问题,那就是原error信息和附加的信息被柔和到一起了。
因为是新创建的error了,在外面处理的时候,就不能使用类型断言了。
为了解决底层异常丢失的问题,可以参考PathError

type PathError struct {
	Op   string
	Path string
	Err  error
}

在进行断言的时候,可以先断言外层error,在断言底层error

	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 实例,并且兼容原有格式动词。

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呢

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值。

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转换成该类型。

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 写入指定的变量中。

  • 14
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值