命令记不住

登陆

链式 error

链式error

  • 新的error类型wrapError
  • 增强了fmt.Errorf()以便拆解warpError
  • 引入了errors.Unwrap()以拆解warpError
  • 引入了errors.Is()用于检查error链条中是否包含指定的错误值
  • 引入了errors.As()用于检查error链条中是否包含指定的错误类型

1.wrapError

wrapError新的error类型

type  wrapError struct {
 msg string //存储上下文信息和err.Error()
 err error  //存储原error
}
func (e *wrapError) Error() string {
    return e.msg
}
func (e *wrapError) Unwrap() error {
    return e.err
}

wrapError初看起来很像前面介绍的os.PathError,os.PathError通过os.PathError.Op和os.PathError.path保存上下文信息,而wrapError和msg成员则把原error和上下文保存到一起,通过err成员个保存原始的error

此处的wrapError与之前的errorString相比,还额外实现了Unwrap()接口,用于返回原始的error

2.fmt,Errorf()

fmt.Errorf()新增了格式动词%w(wrap)用于生成wrapError实例,并且兼容原有格式动词。fmt.Errorf()的实现

fmt.Errorf()将根据格式动词来动态决定生成wrrapError还是errorString.使用%v格式动词生成的error类型仍是errorString(没有实现Unwrap接口),如一下Example测试

func ExampleCreateBasicError() {
    err := errors.New("this is demo error")

    basicErr := fmt.Errorf("some context: %v", err)           // 使用 %v
    if _, ok := basicErr.(interface{ Unwrap() error }); !ok { // 如果errBasic没有实现Unwrap接口
        fmt.Print("basicErr is a errorString")
    }
    // Output:
    // basicErr is a errorString
}

而使用%w格式动词生成的error类型将自动变成wrapError(实现了Unwrap接口),如一下Example所示:

func ExampleCreateWrapError() {
    err := errors.New("this is demo error")

    wrapError := fmt.Errorf("some context: %w", err)          // 使用%w
    if _, ok := wrapError.(interface{ Unwrap() error }); ok { // 如果wrapError实现了Unwrap接口
        fmt.Print("wrapError is a wrapError")
    }
    // Output:
    // wrapError is a wrapError
}

这样,当error在函数间传递时,error之间好像被组织成一个链式结构,使用fmt.Errorf()生成wrapError有两个限制

  • 每次生成wrapError时只能使用一次%w动词
  • %w动词只能匹配实现了error接口的参数

(1)

由wrapError的数据结构可知,每个wrapError只能包含一个原始error信息,所以fmt.Errorf()函数每次只能接受一个%w动词

wrapError := fmt.Errorf("sone context %w, %w",err,err)

编译器将给出“Errorf call has more than one error-wrapping directive %w”的错误提示

(2)%w只匹配error参数

由于格式动词%w专用于生成链式的error,所以fmt.Errorf()函数只能匹配实现了error接口的参数

wrapError := fmt.Errorf("sone context: %w","some string")

编译器将给出“Errorf format %w has arg "some string” of wrong type string"的错误提示

另外需要注意的是,虽然wrapError实现了Unwrap()接口,但由于error接口仍然只定义了一个Error()方法,所以使fmt.Errorf()生成的error,不能直接调用自身的Unwrap()接口获得原始error,而需要使用errors包中提供的Unwrap()方法

3.errors.Unwrap()

Unwrap()函数很形象的揭示了其作用,如果把error比作一件衣服,fmt.Errorf()(使用%w)就好比给error增加了意见外套,而Unwrap()函数则是脱掉外套

如果参数err没有实现Unwrap()函数,则说明是基础error,直接返回nil,否则调用原err实现的Unwrap()函数并返回

对于自定义的error类型,在实现Error()函数的基础上,需要额外实现Unwrao函数,可以升级成链式error,如os.PathError:

type PathError struct {
    Op  string
    Path string
    Err string
}
func (e *PathError) Error()string{
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}
func (e *PathError) Unwrap() error {
    return e.Err
}

使用Unwrap获取原始error并进行比较的例子如下:

func ExampleUnwrap() {
    err := fmt.Errorf("write file error: %w", os.ErrPermission)
    if errors.Unwrap(err) == os.ErrPermission {
        fmt.Printf("permission denied")
    }
    // Output:
    // permission denied
}

在上面的例子中原始的error(os.ErrPermission)只被包裹了一层,我们可以使用Unwrap()函数获取原始的error,在实际的应用中,原始的error可能在层层的函数调用中被包裹了多层,如何进行判断?对此,我们可以循环调用errors.Unwrap()方法来逐层检查

如下:

func ExampleUnwrapLoop() {
    err1 := fmt.Errorf("write file error: %w", os.ErrPermission)
    err2 := fmt.Errorf("write file error: %w", err1)

    err := err2
    for {
        if err == os.ErrPermission {
            fmt.Printf("permission denied")
            break
        }
        if err = errors.Unwrap(err); err == nil {
            break
        }
    }
    // Output:
    // permission denied
}

可以不使用上面的循环,errors包的ls()方法对此提供了更好的支持

4.errors.Is()

errors.Is用于检查特定的error链中是否包含指定的error值,error.Is的伪代码如下:

func Is(err, target error)bool {
    for {
        if err == target{
            return true
        }
        //自定义的error类型如果实现了Is方法,则会咨询Is()方法
        if x,ok := err.(interface{ Is(error) bool });ok && x.Is(target){
            return true
        }
        if err = Unwrap(err);err == nil{
            return false
        }
    }
}

errors.Is()逐层拆解参数err并于参数target对比,如果发现相等则返回true,否则返回false.对于自定义error类型来说,如果实现了Is()方法,则此处在比较时会先咨询自身的Is()方法

使用errors.Is()方法优化上面的Example,代码将变得格外简洁:

func ExampleIs() {
    err1 := fmt.Errorf("write file error: %w", os.ErrPermission)
    err2 := fmt.Errorf("write file error: %w", err1)

    if errors.Is(err2, os.ErrPermission) {
        fmt.Printf("permission denied")
    }
    // Output:
    // permission denied
}

5.errors.As()

检查error时往往还会做类型转换,即使用断言来检测error是否是指定类型,比如:

if e,ok := err.(*os.PathError);ok{
 fmt.Printf("it's an os.PathError, operation; %s, path: %s, msg: %v\n",e.Op, e.Path, e.Err)
}

类似的,如果err是一个链式error,那么上面的代码将不再有效,比如下面的Example将没有任何打印结果:

unc ExampleAssertChanErrorWithoutAs() {
    err := &os.PathError{
        Op:   "write",
        Path: "/root/demo.txt",
        Err:  os.ErrPermission,
    }

    err2 := fmt.Errorf("some context: %w", err)
    if e, ok := err2.(*os.PathError); ok { // 判断失效
        fmt.Printf("it's an os.PathError, operation: %s, path: %s, msg: %v\n", e.Op, e.Path, e.Err)
    }
    // Output:
    //
}                                                     

errors.As()用于从一个error链中查找是否有指定的类型出现,如有,则把error转换成该类型。errors.As()的实现代码如下:

func As(err error,target interface{}) bool {
    val := reflectlite.TYpeOf(err).AssignableTo(target)
    typ := val.Type()
    targetType := typ.Elem()
    for err !=nil {
        if reflectlite.TypeOf(err).AssignableTo(targetType) //如果类型匹配,则将err赋值
      
        val.Elem().Set(reflectlite.ValueOf(err))
        return true  
    }
    if x,ok := err.(interface{As(interface{}) bool });ok && x.As(target) { //如果err实现了As()方法,那么也会咨询As()
        return true
        }
    err = Unwrap(err)     
    }
         return false
}

与Is()的实现非常类似,As()会逐层拆解error链并尝试匹配指定类型,如果类型匹配,则将err值写入参数target.同时,如果error实现了As()函数,那么也尝试咨询As()函数

使用errors.AAs()优化上面的代码:

func ExampleAssertChanWithAs() {
    err := &os.PathError{
        Op:   "write",
        Path: "/root/demo.txt",
        Err:  os.ErrPermission,
    }

    err2 := fmt.Errorf("some context: %w", err)
    var target *os.PathError
    if errors.As(err2, &target) { // 逐层剥离err2并检测是否是os.PathError类型
        fmt.Printf("it's an os.PathError, operation: %s, path: %s, msg: %v\n", target.Op, target.Path, target.Err)
    }
    // Output:
    // it's an os.PathError, operation: write, path: /root/demo.txt, msg: permission denied
}

6.小结

通过扩展fmt.Errorf()来支持创建链式的error,并通过errors.Unwrap()来拆解error

errors.Is()递归地拆解error并检查是否指定的error值,类型于基础error中的哨兵检查,errors.As()递归地拆解error并检查是否是指定的error类型,如是则将error写入指定的变量中

推荐阅读