当前位置: 首页 > 科技观察

抢先看,Go2Error的奋斗之路

时间:2023-03-20 23:20:06 科技观察

本文转载自微信公众号《我的大脑是炸鱼》,作者陈建宇。转载本文请联系脑筋急转弯公众号。大家好,我是炸鱼。Go语言在国内流行以来,除了泛型之外,第二个让人头疼的就是Go处理错误的方式。一个经典的iferr!=nil代码可以识别你是一个Go语言爱好者。自然大家对Goerror的关注度更高,Go团队也是如此。因此在Go2DraftDesigns中官方提到了错误处理(errorhandling)的相关草案,希望以后能正式解决这个问题。今天的文章,我们就一起追踪Go2的失误,看看他是如何“挣扎”的,能否破局?为什么要投诉Go1?抱怨Go1error,首先得知道为什么大家都在喷Error,处理哪里不好。在Go语言中,error其实只是Error的一个接口:typeerrorinterface{Error()string}实际应用场景如下:funcmain(){x,err:=foo()iferr!=nil{//handleerror}}单纯看这个例子,好像没什么问题,但是工程大的时候呢?很明显,iferr!=nil的逻辑会在项目代码中堆积起来。Go代码中如果err!=nil甚至会达到项目代码的30%以上:funcmain(){x,err:=foo()iferr!=nil{//handleerror}y,err:=foo()iferr!=nil{//handleerror}z,err:=foo()iferr!=nil{//handleerror}s,err:=foo()iferr!=nil{//handleerror}}暴力对比,发现四行函数调用,十二行错误,还得苦练熟练IDE中的快速折叠功能还是比较麻烦的。另外,既然是错误处理,肯定不只是returnerr。在工程实践中,项目代码是层层嵌套的。如果直接写成:iferr!=nil{returnerr},在实际工程中肯定是行不通的。你怎么知道哪里抛出了错误信息,真正出错的时候你只能猜测。大家又想出了PlanB,就是添加各种描述信息:iferr!=nil{logger.Errorf("friedfisherrorerr:%v",err)returnerr}虽然看起来像人,但实际上会报错当你这样做的时候,你也会遇到新的问题,因为你要检查错误是从哪里抛出的。没有调用栈,几句话很难定位错误。这时候就会发展成到处都是错误日志:funcmain(){err:=bar()iferr!=nil{logger.Errorf("barerr:%v",err)}...}funcbar()error{_,err:=foo()iferr!=nil{logger.Errorf("fooerr:%v",err)returnerr}returnnil}funcfoo()([]byte,error){s,err:=json.Marshal("helloworld.")iferr!=nil{logger.Errorf("json.Marshalerr:%v",err)returnnil,err}returns,nil}虽然到处打日志,但是会出现很多错误日志,一次出问题了,人肉可能短时间内无法识别。最常见的就是去IDE里搜索ctrl+f哪里出错了。同时,在实际应用中,我们会自定义一些错误类型。Go中需要进行各种判断和处理:iferr:=dec.Decode(&val);err!=nil{ifserr,ok:=err.(*json.SyntaxError);ok{...}returnerr}你要判断不等于nil,还得断言自定义错误类型,总体比较麻烦。总结一下,Go1错误处理的问题至少有:在工程实践中,iferr!=nil写起来很烦人,代码中大量的错误处理判断占了相当大的比例,不够优雅。在排查问题的时候,Go的err没有其他的堆栈信息,所以只能自己添加描述信息,一层一层的堆起来,打很多log,排查起来很麻烦。在验证和测试错误时,需要自定义错误(各种判断和断言)或者被迫使用字符串验证。2019年9月,Go1.13正式发布。其中比较关注的两个是包依赖管理Go模块的转换,以及错误处理errors标准库的改进:Errorwrapping在这次改进中,errors标准库引入了WrappingError的概念,增加了Is/的三个方法As/Unwarp用于对返回错误进行二次处理和识别。同时提前实现了Go2错误预规划中不破坏Go1兼容性的相关功能。简单来说,在Go1.13之后,Go错误可以嵌套,提供三种匹配方式。例子:funcmain(){e:=errors.New("脑子炸了")w:=fmt.Errorf("快抓:%w",e)fmt.Println(w)fmt.Println(errors.Unwrap(w))}输出结果:$gorunmain.go抓紧时间:你的大脑在煎鱼。你的大脑正在煎鱼。在上面的代码中,变量w是一个嵌套的错误。最外层是“quickcatch:”,这里调用%w表示嵌套生成WrappingError。所以它最终输出“抓住它:大脑被炸了”。需要注意的是Go并没有提供Warp方法,而是直接扩展了fmt.Errorf方法。由于下面的输出直接调用了errors.Unwarp方法,会“取出”一层嵌套,最后直接输出“脑子炸了”。在对WrappingError有了基本的了解之后,我们简单介绍一下三个配套的方法:funcIs(err,targeterror)boolfuncAs(errerror,targetinterface{})boolfuncUnwrap(errerror)errolerrors.Is方法签名:funcIs(err,targeterror)boolmethodExample:funcmain(){if_,err:=os.Open("不存在");err!=nil{iferrors.Is(err,os.ErrNotExist){fmt.Println("filedoesnotexist")}else{fmt函数.Println(err)}}}errors.Is方法的作用是判断传入的err和target是否为同一类型,如果是则返回true。errors.As方法签名:funcAs(errerror,targetinterface{})bool方法示例:funcmain(){if_,err:=os.Open("non-existing");err!=nil{varpathError*os.PathErroriferrors.As(err,&pathError){fmt.Println("Failedatpath:",pathError.Path)}else{fmt.Println(err)}}}errors.As方法是从err错误链中识别出与target相同的类型,返回如果赋值是可能的,则为真。errors.Unwarp方法签名:funcUnwrap(errerror)错误方法示例:funcmain(){e:=errors.New("我的脑子是炸鱼")w:=fmt.Errorf("快抓:%w",e)fmt.Println(w)fmt.Println(errors.Unwrap(w))}该方法的作用是解析出嵌套的错误。如果有多层嵌套,需要多次调用Unwarp方法。pkg/errorsGo1的错误处理肯定有很多问题,所以在Go1.13之前,一些“乡亲”发现在没有上下文调试信息的实际工程应用中存在严重的体感问题。因此github.com/pkg/errors诞生于2016年,该库已经受到了很多关注。官方示例如下:.StackTrace()fmt.Printf("%+v",st[0:2])//toptwoframes//Exampleoutput://github.com/pkg/errors_test.fn///home/dfc/src/github.com/pkg/errors/example_test.go:47//github.com/pkg/errors_test.Example_stackTrace///home/dfc/src/github.com/pkg/errors/example_test.go:127简单来说就是为了Go1错误上下文处理进行了优化和处理,比如类型断言,调用堆栈等,有兴趣的可以去github.com/pkg/errors自行学习。另外,你可能会发现Go1.13中新的WrappingError系统与pkg/errors有点相似。你没有看错,Go团队已经接受了相关意见并对Go1进行了调整,但是由于综合原因暂时没有包含调用栈。Go2错误解决了什么问题?前面我们讲了Go1error的很多问题,以及Go1.13和pkg/errors的自救整合。你可能会想,那……Go2error还有机会上场吗?即使Go1做了这些事情,难道Go1报错还是有问题吗?一直没有解决,iferr!=nil还是一梭子,目前社区的声音还是我觉得Go语言的错误处理有待提高。2018年8月,Go2错误提案正式公布了Go2DraftDesigns,其中包含对泛型和错误处理机制的改进的初步草案:Go2DraftDesigns注:Go1.13将正式引入一些不破坏Go1兼容性的Error特性Added到主分支,也就是前面提到的WrappingError。错误处理(ErrorHandling)首先要解决的问题是大量的iferr!=nil问题,Go2错误处理的设计草案就是为此提出来的。简单例子:iferr!=nil{returnerr}优化方案如下:funcCopyFile(src,dststring)error{handleerr{returnfmt.Errorf("copy%s%s:%v",src,dst,err)}r:=checkos.Open(src)deferr.Close()w:=checkos.Create(dst)handleerr{w.Close()os.Remove(dst)//(onlyifacheckfails)}checkio.Copy(w,r)checkw.Close()returnnil}主函数:funcmain(){handleerr{log.Fatal(err)}hex:=checkoutil.ReadAll(os.Stdin)data:=checkparseHexdump(string(hex))os.Stdout.Write(data)}这个提议引入了两种新的语法形式,第一种是check关键字,它可以选择一个表达式checkf(x,y,z)或者checkerr,这将把这个标记为显式错误检查。其次引入了handle关键字,用来定义errorhandler的流程,一步步往上抛,以此类推,直到handler执行完return语句,就正式结束了。ErrorPrinting第二个要解决的问题是ErrorValues和ErrorInspection的问题,这就导致了ErrorPrinting的问题,也可以认为是格式错误不方便。针对于此,官方提出了ErrorValues和ErrorPrinting的设计草案。一个简单的例子如下:iferr!=nil{returnfmt.Errorf("writeusersdatabase:%v",err)}优化后的方案如下:packageerrorstypeWrapperinterface{Unwrap()error}funcIs(err,targeterror)boolfuncAs(typeE)(errerror)(eE,okbool)该提案增加了错误链的WrappingError概念,同时增加了errors.Is和errors.As的方法,与上述Go1.13的改进一致,此处不再赘述。需要注意的是,Go1.13并没有实现%+v输出调用栈的要求,因为这会破坏Go1的兼容性,造成一些性能问题,Go2中很可能会加入。try-catch不好吗?社区中还有一个声音指出,Go语言是反人类的,没有使用try-catch机制。社区中也引起了很多讨论。详情请见相关提案Proposal:Go内置的错误检查功能,“try”。目前该提案已被否决,详见go/issues/32437#issuecomment-512035919和为什么Go没有例外。小结在本文中,我们介绍了Go1Error的现状,总结了Go语言错误处理的常见问题和看法。同时也介绍了Go团队这几年对Go2和Go1.13Error的不断优化和探索。