Go语言的errors

Go语言的errors包有4个方法:

  • errors.As
  • errors.Is
  • errors.New
  • errors.Unwrap

本期我们来揭开他们的神秘面纱。俗话说的好,柿子还得挑软的捏,按照国际惯例我们先从最简单的New函数开始。

在 Go 源码目录的 errors 目录下,有errors.gowrap.go两个文件,以及对应的errors_test.gowrap_test.go两个单元测试文件。要学一个新东西时,其实看xxx_test.go是一个非常不错的选择,它会告诉你这些函数该怎么用。说回errors.New函数,它在errors.go文件中定义,其他3个函数都在wrap.go文件中。

errors.go 文件源码如下:

package errors

func New(text string) error {
	return &errorString{text}
}

type errorString struct {
	s string
}

func (e *errorString) Error() string {
	return e.s
}

去掉注释就剩这点内容了,代码就不做解释了。看下Go简洁的编程哲学,errors包中定义了New函数,而没有用NewError,因为Go通过包名调用函数的特性,包命已经说明了上下文。

在这里插入图片描述


第二个我们来看errors.Unwrap函数,它的源码如下:

func Unwrap(err error) error {
	u, ok := err.(interface {
		Unwrap() error
	})
	if !ok {
		return nil
	}
	return u.Unwrap()
}

乍一看好像不明所以,我们把上面的代码重写一下:

type Wraper interface {
	Unwrap() error
}

func Unwrap(err error) error {
	u, ok := err.(Wraper)
	if !ok {
		return nil
	}
	return u.Unwrap()
}

现在就清楚多了,首先断言err是否实现了匿名接口interface{ Unwrap() error },如果实现了这个接口就调用它的Unwrap函数,否则返回nil。通过Unwrap函数,就可以生成一条错误链。

在这里插入图片描述


再来看errors.Is函数。源码如下:

// Is用来判断错误链中是否有错误和target匹配.
// 错误链由err以及重复调用Unwrap得到的error组成。
// err和target匹配的条件是他俩相等或者err实现了Is函数并且该函数返回true.
// 通过提供Is方法,一个错误类型可以和一个现有错误等同。例如,MyError定义了如下方法:
//	func (m MyError) Is(target error) bool { return target == os.ErrExist }
// 此时 Is(MyError{}, os.ErrExist) 返回true. 参见标准库的syscall.Errno.Is.
func Is(err, target error) bool {
	if target == nil {
		return err == target
	}

	isComparable := reflectlite.TypeOf(target).Comparable()
	for {
		if isComparable && err == target {
			return true
		}
		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
			return true
		}
		// TODO: consider supporting target.Is(err). This would allow
		// user-definable predicates, but also may allow for coping with sloppy
		// APIs, thereby making it easier to get away with them.
		if err = Unwrap(err); err == nil {
			return false
		}
	}
}

Is函数中也有一个匿名接口interface{ Is(error) bool },逻辑还是很简单的,唯一吸引眼球的是它的反射用的是reflectlite包,而不是我们熟知的reflect包。reflectlite是一个内部使用的包,并不对外公开,基本套路和reflect大同小异,以你对reflaect包的理解套用到reflectlite包也是可以的。

在这里插入图片描述


最后看errors.As函数。源码如下:

// 寻找错误链中第一个和target匹配的error,如果能找到,将err的值设置到target并返回true,否则返回false。
// 错误链由err以及重复调用Unwrap得到的error组成。
// 如果err可以赋值给target或者err有As(实现了匿名接口)函数并且As函数返回true,则err和target是匹配的。
// 在后一种情况下,由As函数负责设置target。
// 提供了As方法的error可以视为另一种类型的错误。
// 如果target既不是error类型也不是接口类型,As函数会panic
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")
	}
	if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
		panic("errors: *target must be interface or implement error")
	}
	targetType := typ.Elem()
	for err != nil {
		if reflectlite.TypeOf(err).AssignableTo(targetType) {
			val.Elem().Set(reflectlite.ValueOf(err))
			return true
		}
		if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
			return true
		}
		err = Unwrap(err)
	}
	return false
}

var errorType = reflectlite.TypeOf((*error)(nil)).Elem()

As 函数就要复杂一点了,不过流程还是很清晰的,和Is函数的流程一般无二。这里用到了较多的反射,我们可以看下当err和target匹配时的处理val.Elem().Set(reflectlite.ValueOf(err))

val是target反射出的ValueElem方法可以获取元素值的Value,具体来说就是接口包含的值、指针指向的值、以及容器类型(切片|map|通道)包含的值。当然reflectlite包的Elem函数只支持接口和指针,这也是它和reflect包不同的地方,也因此As方法要求target必须是指针类型。如果你打开Elem函数的实现,就能看到下面的注释:

// Elem returns the value that the interface v contains or that the pointer v points to.
// It panics if v's Kind is not Interface or Ptr.
// It returns the zero Value if v is nil.

三句英文清晰明了,相信不用我再翻译了。言归正传,当err和target匹配时,将err反射出来的Value设置给了target。我们知道Value结构体有三个字段:

  • 一个rtype类型的指针,表示类型信息
  • 一个unsafe.Pointer,指向数据的指针
  • 一个flag

那么ValueSet方法干了啥呢?不妨看下源代码:

// Set assigns x to the value v.
// It panics if CanSet returns false.
// As in Go, x's value must be assignable to v's type.
func (v Value) Set(x Value) {
	v.mustBeAssignable()
	x.mustBeExported() // 防止泄露非导出字段
	var target unsafe.Pointer
	if v.kind() == Interface {
		target = v.ptr
	}
	x = x.assignTo("reflectlite.Set", v.typ, target)
	if x.flag&flagIndir != 0 {
		typedmemmove(v.typ, v.ptr, x.ptr)
	} else {
		*(*unsafe.Pointer)(v.ptr) = x.ptr
	}
}

先不要对flagIndirtypedmemmove感到疑惑,ifelse分支的目的都是一样的,那就是替换Value中那个指向数据的指针ptr

在这里插入图片描述

所以当err和target匹配时,As函数用err的值替换了target的数据,但是保留了target的类型。

最后我们再来看flagIndirtypedmemmove的问题。flagIndir是个啥在Value的源码中有说明:

type Value struct {
	typ *rtype

	// Pointer-valued data or, if flagIndir is set, pointer to data.
	// Valid when either flagIndir is set or typ.pointers() is true.
	ptr unsafe.Pointer

	// flag holds metadata about the value.
	// The lowest bits are flag bits:
	//	- flagIndir: val holds a pointer to the data
	flag
}

type flag uintptr

const (
	flagKindWidth        = 5 // there are 27 kinds
	flagKindMask    flag = 1<<flagKindWidth - 1
	flagStickyRO    flag = 1 << 5
	flagEmbedRO     flag = 1 << 6
	flagIndir       flag = 1 << 7
	flagAddr        flag = 1 << 8
	flagMethod      flag = 1 << 9
	flagMethodShift      = 10
	flagRO          flag = flagStickyRO | flagEmbedRO
)

flagIndir是一个标识,表示Valueptr字段究竟是指向数据还是指向指针。如果设置了flagIndir,则ptr指向指针,如果没有设置,则ptr指向数据。更多内容可以参见这篇文章。我们还可以通过下面的代码来验证这一点。

type MyValue struct {
	typ uint64
	ptr unsafe.Pointer
	flag
}

type flag uintptr

var flagIndir flag = 1 << 7

func main() {
	var a = 1
	var p = &a
	vala := reflect.ValueOf(a)
	valp := reflect.ValueOf(p)
	myVal := (*MyValue)(unsafe.Pointer(&vala))
	fmt.Printf("%b\n", myVal.flag)
	fmt.Println(myVal.flag & flagIndir)
	fmt.Println("---")
	myVal = (*MyValue)(unsafe.Pointer(&valp))
	fmt.Printf("%b\n", myVal.flag)
	fmt.Println(myVal.flag & flagIndir)
}

//===== 输出 =====//
10000010
128
---
10110
0
//===============//

所以Value.Set函数的逻辑如下:

  • 如果Value指向的是指针,那么可以直接赋值;
  • 如果Value指向的是数据,那么调用typedmemmove函数将x.ptr指向的数据拷贝到v.ptr指向的内存,拷贝多少字节由类型v.typ决定。

typedmemmove函数在源码中只有一个函数定义,应该是一个汇编函数,遗憾的是我只在runtime包找到了memmove函数的汇编实现,没找到typedmemmove的汇编实现。

// typedmemmove copies a value of type t to dst from src.
//go:noescape
func typedmemmove(t *rtype, dst, src unsafe.Pointer)

在这里插入图片描述


相关文章

类型转换 1、int转string 2、string转int 3、string转float ...
package main import s &quot;strings&quot; import...
类使用:实现一个people中有一个sayhi的方法调用功能,代码如...
html代码: beego代码:
1、读取文件信息: 2、读取文件夹下的所有文件: 3、写入文件...
配置环境:Windows7+推荐IDE:LiteIDEGO下载地址:http:...