Go-知识error

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

相关推荐
Ai 编码助手4 小时前
什么是内存溢出,golang是如何解决内存溢出的
jvm·golang
蒙娜丽宁5 小时前
深入探讨Go语言中的切片与数组操作
开发语言·后端·golang·go
小小鱼er5 小时前
go-map系统学习
golang
qq_172805597 小时前
GO HTTP库使用
http·golang·go
u01129006413 小时前
Go语言变量的声明
开发语言·后端·golang
Python私教17 小时前
Go语言现代Web开发03 关键字和包以及基本数据类型
开发语言·golang·xcode
探索云原生18 小时前
ArgoWorkflow教程(四)---Workflow & 日志归档
go·jenkins·devops·cicd·argoworkflow
龙门吹雪1 天前
Go开源日志库Logrus的使用
开发语言·golang·logrus·日志分割·开源日志框架
2401_863671351 天前
Go-ecc加密解密详解与代码_ecdsa
ios·golang·xcode
GoppViper1 天前
golang学习笔记12——Go 语言内存管理详解
笔记·后端·学习·golang·编程语言·内存优化·golang内存管理