美文网首页
2021/05/04关于GO的错误处理(error)

2021/05/04关于GO的错误处理(error)

作者: 温岭夹糕 | 来源:发表于2021-05-05 00:17 被阅读0次

1.首先何谓error

GO中的error就是一个普通的接口(实现了Error方法)

位于源码builtin.go中

type error interface {
        Error() string
}

利用实践验证demo1

//该demo通过基础类型衍生出自定义类型,并类型断言成功
type MyString string 

func (s MyString) Error() string {
    return string(s) 
}

func main(){
    mystring:=MyString("look me")
    if _,ok := interface{}(mystring).(error);ok {
        fmt.Println("is an error")
    }
}

2.如何创建一个error?

2.1.方式一errors.New

//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
}

通过阅读errors源码包我们不难发现,errors.New返回的是一个指针,并且创建错误的具体信息被保存在结构体中的字段中

2.1.1为什么这里返回的是一个指针,而不是一个结构体对象

我们首先需要明确一个事情,结构体类型之间做两两比较的时候即==,判断的是结构体中的所有成员是否相同(如下demo2)

type myTest struct {
    s string
}

var a = myTest{"hi"}

func main(){
    if a== (myTest{"hi"}) {
        fmt.Println("true")//相同
    }
}

也就是说,如果errors.New如果返回的不是指针,那么当我们自定义的错误类型就很容易和别人的产生"碰撞"

想想,我们定义了一个超时错误,同事也定义了一个超时错误,还破天荒的都用相同的结构去描述这个错误,那么结果就是当项目抛出一个超时错误时,两个竟然都可以用==匹配上,这合理吗?

指针保证了地址(错误)的唯一性,基础库中也大量使用了自定义error

//bufio.go
var (
        ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
        ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
        ErrBufferFull        = errors.New("bufio: buffer full")
        ErrNegativeCount     = errors.New("bufio: negative count")
)

2.2方式二fmt.Errorf

基础用法demo3

func main(){
    mystring:=fmt.Errorf("look me")
    fmt.Printf( "%T:%v\n" ,mystring,mystring )
    mystring2:=fmt.Errorf( "%w look me again",mystring)
    fmt.Printf( "%T:%v\n" ,mystring2,mystring2 )
    fmt.Println( mystring==mystring2 )
}
//output
*errors.errorString:look me
*fmt.wrapError:look me look me again
false

fmt.Errorf除了可以抛出一个异常,还可以"封装"一个异常
关于wrapError源码位于fmt/errors.go

type wrapError struct {
        msg string
        err error
}

func (e *wrapError) Error() string {
        return e.msg
}

func (e *wrapError) Unwrap() error {
        return e.err
}

我们可以看到实现了error接口,也可以通过Unwrap方法不断追朔源头(相当于一个单向链表结构)

2.2.1关于go1.14errors包中新增的方法Is和As

errors.Is
当异常被多次封装时,我们上游可以通过Is方法来判断该异常是否是底层的某个异常(实际是递归调用Uwrap,修改demo3)

func main(){
    mystring:=fmt.Errorf("look me")
    mystring2:=fmt.Errorf( "%w look me again",mystring)
    mystring3:=fmt.Errorf( "%w look me again",mystring2)

    fmt.Println( errors.Is(mystring3,mystring2)  )
    fmt.Println( errors.Is(mystring3,mystring)  )

}

源码位于errors/wrap.go

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 { //这里递归调用Unwrap
                        return false
                }
        }
}

errors.As
As所作的就是遍历error链,从里面找到符合类型的error,然后把这个error赋给目标
demo4

type ErrorString struct {
    s string
}

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

func main(){
    var targetErr *ErrorString
    err := fmt.Errorf("new error:[%w]", &ErrorString{s:"target err"})
    fmt.Println(errors.As(err, &targetErr))
}

源码

func As(err error, target interface{}) bool {
    //一些判断,保证target,这里是不能为nil
    if target == nil {
        panic("errors: target cannot be nil")
    }
    val := reflectlite.ValueOf(target)
    typ := val.Type()
    
    //这里确保target必须是一个非nil指针
    if typ.Kind() != reflectlite.Ptr || val.IsNil() {
        panic("errors: target must be a non-nil pointer")
    }
    
    //这里确保target是一个接口或者实现了error接口
    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 {
        //关键部分,反射判断是否可被赋予,如果可以就赋值并且返回true
        //本质上,就是类型断言,这是反射的写法
        if reflectlite.TypeOf(err).AssignableTo(targetType) {
            val.Elem().Set(reflectlite.ValueOf(err))
            return true
        }
        //这里意味着你可以自定义error的As方法,实现自己的类型断言代码
        if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
            return true
        }
        //这里是遍历error链的关键,不停的Unwrap,一层层的获取err
        err = Unwrap(err)
    }
    return false
}

3.关于panic

error的抛出不一定影响我们程序的正常退出,但是panic意味着程序的崩溃
如demo5,即使是协程引发panic,也会造成整个进程的异常退出

func main(){
    defer func(){  //即使是主进程做recover也无法恢复协程引发的panic
        if err:=recover();err!=nil{
           fmt.Println("world") 
        }
    }()
    go func(){
        fmt.Println("hello")
        panic("bye")
    }()
    time.Sleep(3*time.Second) //后续不执行
    fmt.Println("world") 
}

为防止上面这种“野协程”导致整个进程挂掉的情况,我们通常都会有recover进行处理

func main(){
    Go( func(){
        fmt.Println("hello")
        panic("bye")
    } )
    time.Sleep(3*time.Second)
    fmt.Println("world")

}

func Go(x func() ){
    go func(){
        defer func(){ //通过在协程中recover对进程做保护
            if err := recover();err !=nil {
                fmt.Println(err)
            }
        }()
        x()
    }()
}

因此对于那些表示不可恢复的程序错误如索引越界,栈溢出,强依赖资源错误(缺少必须的配置文件导致后续任务无法执行),我们才使用panic,对于其他错误情况,我们期望使用error进行判断

4.优先失败

即函数的返回值若存在error,需优先考虑失败而不是成功,优先处理这个错误

5.错误类型

5.1预定义错误

即使用errors.New预先构造包级别的error
这个在上文介绍过

var (
  TimeOut = errors.New("timeout")
   ...
)

缺点明显,不够灵活,调用方想知道具体信息必须使用error.Error()方法查看,还需通过==等值判断,若异常在传递过程中被包装(携带有意义的上下文信息如fmt.Errorf方法包装 )则无法使用==判断,无法查看堆栈信息(具体是哪个文件哪个函数哪一行抛出)
并且加大了包与包之间的耦合性

5.2自定义类型

我们既然知道了error的接口如何实现,不妨自定义一个error类型来告诉调用方抛出异常时的上下文环境

type MyError struct{
 MSG string
 File string
 Line int
}
func (e *MyError) Error() string { //实现接口
 return ...
}

但是缺点是当抛出一个异常时,我们需要做断言,如果为自定义异常则打印上下文信息

func main(){
 err :=test()
 switch err:=err.(type){
   case nil:
        断言成功 什么都不做,即没有错误
   case *MyError ....
 }
}

5.3非透明错误

即调用方只判断成功或失败

func fn(){
  err :=Foo()
  if err!=nil {
     return err
  }
}

但是我们无法看到内部具体什么错误

5.4断言错误

是对自定义类型的扩展,用接口来约束具体的错误类型

type template interface {
    error
    Timeout() bool //是一个超市错误吗
 }

type MyError struct {
    t bool
    s string
}

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

func (e *MyError) Timeout() bool {
    return e.t
}

func IsTimeout(err error) bool {
    te,ok := err.(template)
    return ok && te.Timeout()
}

func main(){
    err := &MyError{true,"timeout!!!"}
    fmt.Println(  IsTimeout(  err  )  )
}

可以看到template扩展了error接口,通过isTimeout函数来断言异常,如果断言成功则返回是否为超时错误

5.5Wrap Error

原本的error他抛出时几乎不懈怠抛出时的上下文信息,我们通过5.2的自定义错误类型来完善了他,那么当我们中间的调用者(并不是最后)捕获到这个异常时,想要再添加一些上下文信息时,这种方式显然是不够的,我们需要使用fmt.Errorf配合%w来包装error,但是这个包装后的error并不是原本的error需要通过Unwrap方法来追溯源头,这里的追溯源头就是wrap Error的思想,除去fmt.Errorf还有一个更好的wrap error方式就是使用第三方库(github.com/pkg/errors)

github.com/pkg/errors主要对外提供两种方法
Wrap和Cause

源码

func Wrap(err error, message string) error {
    if err == nil {
        return nil
    }
    err = &withMessage{
        cause: err,
        msg:   message,
    }
    return &withStack{
        err,
        callers(),
    }
}
func (w *withStack) Cause() error { return w.error }
func Cause(err error) error {
    type causer interface {
        Cause() error
    }

    for err != nil {
        cause, ok := err.(causer)
        if !ok {
            break
        }
        err = cause.Cause()
    }
    return err
}

我们可以看到这实际也是一个链表,链表的尾部是原始error,通过Wrap方法用链表节点的withMessage和withStack一层层的包装向外传递
解包使用Cause来获取原始错误
更多使用方法参考pkg/errors文档
简单demo6,利用pkg/errors携带堆栈信息

import (
    "fmt"
    "os"
    xerror "github.com/pkg/errors"
)

func C() error {
    _, err := os.Open("abc")
    if err != nil {
        err = xerror.WithStack(err)
        return err
    }
    return nil
}

func main(){
    err:=C()
    if err!=nil{
        fmt.Printf("stack error :%+v",err) //注意这里用%+v确保完整打印
    }
}

推荐使用方式:
和其他库,比如标准库,github第三方库,自己的基础库,进行交互协作时使用Wrap,中间传递直接返回错误,而不是每个错误产生的地方到处打日志,在程序的顶部使用%+v打印堆栈

6.文章参考

1.Golang eror的突围
2.pkg/errors
3.golang中内嵌interface
4.GO中的日常错误对象
5.Error Wrapping深度分析
6.Go语言(golang)的错误(error)处理的推荐方案

相关文章

网友评论

      本文标题:2021/05/04关于GO的错误处理(error)

      本文链接:https://www.haomeiwen.com/subject/kymhdltx.html