Go语言的errors
包有4个方法:
errors.As
errors.Is
errors.New
errors.Unwrap
本期我们来揭开他们的神秘面纱。俗话说的好,柿子还得挑软的捏,按照国际惯例我们先从最简单的New
函数开始。
在 Go 源码目录的 errors 目录下,有errors.go
和wrap.go
两个文件,以及对应的errors_test.go
和wrap_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反射出的Value
。Elem
方法可以获取元素值的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
那么Value
的Set
方法干了啥呢?不妨看下源代码:
// 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
}
}
先不要对flagIndir
和typedmemmove
感到疑惑,if
和else
分支的目的都是一样的,那就是替换Value
中那个指向数据的指针ptr
。
所以当err和target匹配时,As
函数用err的值替换了target的数据,但是保留了target的类型。
最后我们再来看flagIndir
和typedmemmove
的问题。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
是一个标识,表示Value
的ptr
字段究竟是指向数据还是指向指针。如果设置了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)