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

Go错误嵌套是如何实现的?

时间:2023-03-12 03:43:36 科技观察

GoError的设计理念是“错误即价值”。这句话应该怎么理解?翻译起来相当困难。不过从源码来看,似乎更容易理解其背后的含义。GoError的源码很简单,就几行://src/builtin/builtin.gotypeerrorinterface{Error()string}error是一个接口类型,只需要实现Error()方法即可。在Error()方法中,您可以返回自定义结构的任何内容。让我们先谈谈如何创建一个错误。CreateError创建错误有两种方式,分别是:errors.New();fmt.Errorf().errors.New()errors.New()的使用延续了Go一贯的风格,直接点击New即可。举个例子:packagemainimport("errors""fmt")funcmain(){err:=errors.New("Thisisanerrorcreatedbyerrors.New()")fmt.Printf("errerrortype:%T,erroris:%v\n",err,err)}/*Outputerr错误类型:*errors.errorString,erroris:Thisisanerrorcreatedbyerrors.New()*/这段代码唯一容易混淆的部分可能是类型不对,不过没关系。只要看源码,瞬间解决。源码如下://src/errors/errors.go//Newreturnsanerrorthatformatsasthegiventext.//EachcalltoNewreturnsadistincterrorvalueevenifthetextisidentical.funcNew(textstring)error{return&errorString{text}}//errorStringisatrivialimplementationoferror.typeererrorStringstructfrunc(String)*(String)return.s可以看到errorString是一个实现了Error()方法的结构体,New函数直接返回了errorString指针。这种用法很简单,但不切实际。如果我还想返回程序的上下文信息,那是行不通的。我们来看第二种方式。fmt.Errorf()先来看一个例子:)iferr==sql.ErrNoRows{fmt.Printf("datanotfound,%+v\n",err)return}iferr!=nil{fmt.Println("Unknownerror")}}/*输出datanotfound,sql:norowsinresultset*/这个例子输出了我们想要的,但是还不够。一般情况下,我们会使用fmt.Errorf()函数来添加我们想要添加的文本信息,让返回的内容更清晰,处理起来更灵活。因此,foo()函数将改为:funcfoo()error{returnfmt.Errorf("fooerr,%v",sql.ErrNoRows)}这时候问题就出现了。经过fmt.Errorf()的封装,原来的错误类型发生了变化,导致err==sql.ErrNoRows不再成立,返回信息变为Unknownerror。如果你想根据返回的错误类型做不同的处理,那是不行的。因此,Go1.13为我们提供了wrapError来处理这个问题。WrapError看一个例子:packagemainimport("fmt")typemyErrorstruct{}func(emyError)Error()string{return"Errorhappened"}funcmain(){e1:=myError{}e2:=fmt.Errorf("E2:%w",e1)e3:=fmt.Errorf("E3:%w",e2)fmt.Println(e2)fmt.Println(e3)}/*outputE2:ErrorhappenedE3:E2:Errorhappened*/看起来不错乍一看没有区别,但是背后的实现原理不一样。Go扩展了fmt.Errorf()函数以添加一个%w标识符来创建wrapError。//src/fmt/errors.gofuncErrorf(formatstring,a...interface{})error{p:=newPrinter()p.wrapErrs=truep.doPrintf(format,a)s:=string(p.buf)varerrerrorifp.wrappedErr==nil{err=errors.New(s)}else{err=&wrapError{s,p.wrappedErr}}p.free()returnerr}当使用w%时,该函数将返回&wrapError{s,p。wrappedErr},wrapError结构体定义如下:err}实现了Error()方法,说明是错误,Unwrap()方法是获取封装的错误。//src/errors/wrap.gofuncUnwrap(errerror)error{u,ok:=err.(interface{Unwrap()error})if!ok{returnnil}returnu.Unwrap()}它们之间的关系是这样的:因此,我们可以使用w%来修改上面的程序,让它的输出更加丰富。如下:packagemainimport("database/sql""errors""fmt")funcbar()error{iferr:=foo();err!=nil{returnfmt.Errorf("barfailed:%w",foo())}returnnil}funcfoo()error{returnfmt.Errorf("foofailed:%w",sql.ErrNoRows)}funcmain(){err:=bar()iferrors.Is(err,sql.ErrNoRows){fmt.Printf("datanotfound,%+v\n",err)return}iferr!=nil{fmt.Println("Unknownerror")}}/*outputdatanotfound,barfailed:foofailed:sql:norowsinresultset*/终于有了满意的输出结果,每两个函数已经添加了必要的上下文信息,也符合错误类型的判断。errors.Is()函数用于判断err及其封装的错误链是否包含目标类型。这样也就解决了上面说的无法判断错误类型的问题。后记事实上,Go目前对Error的处理也充满了争议。不过,官方团队正在积极与社区沟通,并提出改进的方法。相信在不久的将来,会找到更好的解决办法。在这个阶段,大多数团队可能会选择github.com/pkg/errors包来进行错误处理。如果你有兴趣,你可以从中学习。