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

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)处理的推荐方案

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容