大家好,这里是每周陪你进步的网管。之前写过几篇关于Go错误处理的文章,发现文章中的很多知识点都有些落伍了。例如,Go在1.13之后增加了一些对错误处理的支持。最大的变化是对ErrorWrapping的支持。以前都是使用库“github.com/pkg/errors”来打包调用链接函数中的错误。Go在2019年发布的Go1.13版本也采用了错误打包,同时也提供了几个实用的实用函数,方便我们更好的使用错误打包。这篇文章会主要讲这方面的知识点,但是一开始我们会强调再次使用GoError的误区,以免我们从其他语言转过来的时候给自己挖坑。估计很多人都知道自定义错误需要实现错误接口,但是在文章开头,我们会从这个约定开始,因为我之前在PHP-to-Go研发团队工作过,大家可能一开始不是很好。这是错误的使用方法。首先我们再重复一遍,Go用错误类型的值来表示程序中的错误。errortype是一个内置的接口类型,它只指定一个返回字符串值的Error方法。typeerrorinterface{Error()string}Go程序函数经常返回一个错误值packagestrconvfuncAtoi(sstring)(int,error){....}调用者通过测试error值是否为nil来执行测试错误处理。i,err:=strconv.Atoi("42")iferr!=nil{fmt.Printf("无法转换数字:%v\n",err)return}fmt.Println("转换后的整数:",i)当error为nil时,表示成功;如果错误不为零,则表示失败。说完了Go中error最基本的用法,接下来说说项目中的自定义错误类型。如果项目在Dao层定义了这样一个错误类型来记录数据库查询错误。typeMyErrorstruct{SqlstringParamstringErrerror}如果这个自定义的MyError没有实现error接口,Dao层的所有函数都会返回MyError。funcFindUserRowByPhoneMyError(userIdint)(userUser,MyErrorerror){......}然后使用这些Dao函数的代码逻辑层必须引入dao.MyError的extra类型。有人会说,我把MyError定义在public包里,所有的代码逻辑层和Dao层都用这个common.MyError一点问题都没有。乍一看用起来没什么问题,但实际上最大的问题是不兼容,不符合Go语言的接口约束报错,所以无法使用Go提供的其他函数来做errors为自定义错误类型,例如后面要介绍的错误包装。所以对于自定义的错误类型,我们还需要通过实现错误接口定义的方法,使其成为真正的Go错误。func(e*MyError)Error()string{returnfmt.Sprintf("sql:%s,params:%s,err:%s",e.Sql,e.Param,e.Err.Error())}Wrappingerrors在实际的程序应用中,一个逻辑往往需要调用多层函数才能完成。在程序中,我们建议ErrorHandling尽量交给上层调用函数处理。中间和底部函数将自己包装起来。要记住的错误消息附加到原始错误并返回给外部函数。例如,像这样的东西:funcdoAnotherThing()error{returnerrors.New("errordoinganotherthing")}funcdoSomething()error{err:=doAnotherThing()returnfmt.Errorf("errordoingsomething:%v",err)}funcmain(){err:=doSomething()fmt.Println(err)}从打印错误信息的输出看这段代码没有错,但是深层次的问题很明显,我们输了原来的err,因为它已经被我们的fmt.Errorf函数转换成一个新的字符串。基于这样的背景,很多开源的第三方库都提供了错误打包、添加错误调用栈等功能。使用最多的库是“github.com/pkg/errors”,它提供了以下主要的打包错误的功能。//只追加新信息funcWithMessage(errerror,messagestring)error//只追加调用栈信息funcWithStack(errerror)error//同时追加栈和信息funcWrap(errerror,messagestring)errorGo官方在2019年发布了1.13版本,也加入了对错误打包的支持,但没有提供任何Wrap功能,而是扩展了fmt.Errorf功能,并添加了一个%w来产生一个wrapping错误。e:=errors.New("Originalerror")w:=fmt.Errorf("Anerrorwaswrappedoutside%w",e)Go1.13引入了打包错误后,也为内置的添加了一个包errors包了3个函数,分别是Unwrap、Is和As。让我们先谈谈Unwrap。顾名思义,它的作用就是获取封装错误中的嵌套错误。funcUnwrap(errerror)error{//先判断是否是wrappingerroru,ok:=err.(interface{Unwrap()error})//如果不是,returnnilif!ok{returnnil}//否则然后调用这个错误的Unwrap方法返回嵌套的错误returnu.Unwrap()}这里需要注意的是嵌套可以有很多层。我们调用了一次errors.Unwrap函数只能返回下一层的错误,如果你想得到更多的里面,你需要多次调用errors.Unwrap函数。最后,如果错误不是扭曲错误,则返回nil。如果想得到最原始的错误,建议自己封装一个效用函数,像这样funcCause(errerror)error{forerr!=nil{err=errors.Unwrap(err)}returnerr}为我们文章开头定义的自定义错误MyError如果想把它变成一个包装Error,还需要实现一个Unwrap()方法。func(e*MyError)Unwrap()error{returne.Err}出现打包错误后,需要跟进和纠正一些具体的错误判断和错误的类型转换。这是1.13之后errors包中添加的另外两个工具函数Is和As的作用。让我们一一谈谈。Go1.13之前没有打包errors.Is的时候,直接判断错误是不是程序中的同一个错误:iferr==os.ErrNotExists{......}这样我们就可以判断dosomething了。但是现在有了封装错误,这个方法并不完美,因为你不知道返回的err是不是嵌套错误,嵌套了好几层。所以基于这种情况,Go为我们提供了errors.Is函数。funcIs(err,targeterror)bool如果err和targeterrortarget相同则返回true。如果err是一个包装错误并且目标错误target也包含在此嵌套错误链中,则也返回true。下面是使用errors的例子。就是判断是否是同一个错误。varErrDivideByZero=errors.New("除以零")funcDivide(a,bint)(int,error){ifb==0{return0,ErrDivideByZero}returna/b,nil}funcmain(){a,b:=10,0结果,err:=Divide(a,b)iferr!=nil{switch{caseerrors.Is(err,ErrDivideByZero):fmt.Println("除以零错误")default:fmt.Printf("unexpecteddivisionerror:%+v\n",err)}return}fmt.Printf("%d/%d=%d\n",a,b,result)}errors.As也没有在对错误进行打包之前,我们需要将错误转化为具体的错误类型,通常使用类型断言或者类型切换,其实就是类型断言。如果pathErr,ok:=err.(*os.PathError);ok{fmt.Println(pathErr.Path)}但是出现打包错误后,返回的err可能已经嵌套了,无法使用该方法,所以Go在errors包中为我们提供了As函数。funcAs(errerror,targetinterface{})boolAs函数所做的就是遍历嵌套的错误链,从中找到符合类型的错误,然后将这个错误赋值给target参数,这样我们就可以在程序中使用转换后的目标,因为这里有赋值,所以目标必须是指针。这也是Go内置包中的约定,如json.Unmarshal。所以用As函数实现上面的例子就变成酱姨了:varpathErr*os.PathErroriferrors.As(err,pathErr){fmt.Println(pathErr.Path)}总结这篇文章主要是更新Error处理对于新Go1.13之后的新增功能,之前的文章介绍的更多的是“pkg/errors”包的使用方式,主要是公司使用的Go版本一直是两年前的1.12,所以这部分知识我还没有已更新,所以这里是一个简短的总结。
