:如果你没有被错误绊倒,那么当你被绊倒时,你会哭泣并记住为什么你没有思考和反思错误处理这样一个宏大的话题当年实践Golang错误处理Golang有很多好的地方,这也是它如此受欢迎的主要原因。但是Go1对错误处理的支持过于简单,以至于在日常开发中会带来很多不便,被很多开发者吐槽。这些缺陷导致了几个开源解决方案。同时,Go官方也在语言和标准库层面进行改进。本文将给出几种常见的错误创建方式并分析一些常见问题,比较各种解决方案,并展示迄今为止的最佳实践(转1.13)。产生错误的几种方式先介绍几种常见的产生错误的方式String-basederrorserr1:=errors.New("math:squarerootofnegativenumber")err2:=fmt.Errorf("math:squarerootofnegativenumber%g",x)withdatacustomerrorpackageserrimport("fmt""github.com/satori/go.uuid""log""runtime/debug""time")//自定义基本错误类型,messageArgs...interface{})BaseError{returnBaseError{InnerError:err,Message:fmt.Sprintf(message,messageArgs),StackTrace:string(debug.Stack()),Misc:make(map[string]接口{}),}}func(err*BaseError)Error()string{//实现Error接口returnerr.Message}//具体使用//“中间”moduletypeIntermediateErrstruct{error}funcrunJob(idstring)error{constjobBinPath="/bad/作业/二进制“isExecutable,err:=isGloballyExec(jobBinPath)iferr!=nil{returnIntermediateErr{wrapError(err,“cannotrunjob%q:requisitebinariesnotavailable”,id,)}}elseifisExecutable==false{returnwrapError(nil,"cannotrunjob%q:requisitebinariesarenotexecutable",id,)}returnexec.Command(jobBinPath,"--id="+id).Run()}抛出问题在开发中,经常需要对返回的错误值进行检查,并进行相应的处理。下面给出了最简单的示例。import("database/sql""fmt")funcGetSql()error{returnsql.ErrNoRows}funcCall()error{returnGetSql()}funcmain(){err:=Call()iferr!=nil{fmt.Printf("goterr,%+v\n",err)}}//Outputs://goterr,sql:norowsinresultset有时需要根据返回的错误类型做不同的处理,例如:import("database/sql""fmt")funcGetSql()error{returnsql.ErrNoRows}funcCall()error{returnGetSql()}funcmain(){err:=Call()iferr==sql.ErrNoRows{fmt.Printf("datanotfound,%+v\n",err)return}iferr!=nil{//Unknownerror}}//Outputs://datanotfound,sql:norowsinresultset实际中往往需要在错误返回前加上上下文信息,以便调用者了解错误场景.比如Getcall方法经常写成:funcGetcall()error{returnfmt.Errorf("GetSqlerr,%v",sql.ErrNoRows)}但是此时err==sql.ErrNoRows是不成立的。另外,以上写法都在返回错误时丢失了调用栈的重要信息。我们需要更灵活和通用的方法来处理此类问题。解决方案针对现有的不足,目前有以下几种解决方案。这些方法可以根据上下文包装错误,携带原始错误信息,并尽可能保留完整的调用堆栈。方案一:github.com/pkg/errors如果只有错误的文字,我们很难定位到具体的错误位置。虽然可以通过搜索错误文本的代码来找出问题所在,但信息是有限的。因此,在实践中,我们往往会附上错误发生时的调用堆栈信息。调用堆栈对消费者来说毫无意义。从隔离和自治的角度来看,消费者唯一需要关心的就是错误文本和错误类型。调用堆栈对实现者本身很有价值。因此,如果一个方法需要返回错误,我们一般会使用errors.WithStack(err)或者errors.Wrap(err,"custommessage")等方法将当前调用栈添加到error中,并在一个统一的Logs中记录在本地,方便开发者快速定位问题。Wrap方法用于包装底层错误、添加上下文文本信息并附加调用堆栈。一般用于包装对第三方代码(标准库或第三方库)的调用。WithMessage方法仅添加上下文文本信息,不添加调用堆栈。如果您确定错误已被包装或不关心调用堆栈,请使用此方法。注意:不要重复Wrap,会导致调用栈重复。Cause方法用于判断底层错误。现在我们使用这三个方法重写上面的代码:funcCall()error{returnerrors.WithMessage(GetSql(),"barfailed")}funcmain(){err:=Call()iferrors.Cause(err)==sql.ErrNoRows{fmt.Printf("datanotfound,%v\n",err)fmt.Printf("%+v\n",err)return}iferr!=nil{//unknownerror}}/*输出:datanotfound,Callfailed:GetSqfailed:sql:norowsinresultsetsql:norowsinresultsetmain.GetSql/usr/three/main.go:11main.Call/usr/three/main.go:15main.main/usr/three/main.go:19runtime.main...*/从输出可以看出,使用%v作为格式参数,错误消息保留在一行中,而该行又包含调用堆栈的上下文文本。使用%+v将输出完整的调用堆栈详细信息。如果不需要添加额外的上下文信息,可以只附加调用堆栈后使用WithStack方法:当传入的err参数为nil时,会返回nil,也就是说我们在调用这个方法之前不需要做nil判断,保持代码简洁解决方案二:golang.org/x/xerrors结合社区收到反馈后,Go团队开始考虑简化处理的Go2Proposals中的错误。Go核心团队成员RussCox在xerrors中部分实施了该提案。它解决了同样的问题,思路与github.com/pkg/errors类似,引入了一个新的fmt格式化动词:%w,使用Is进行判断。import("database/sql""fmt""golang.org/x/xerrors")funcCall()error{iferr:=GetSql();err!=nil{returnxerrors.Errorf("barfailed:%w",GetSql())}returnil}funcGetSql()error{returnxerrors.Errorf("GetSqlfailed:%w",sql.ErrNoRows)}funcmain(){err:=Call()ifxerrors.Is(err,sql.ErrNoRows){fmt.Printf("datanotfound,%v\n",err)fmt.Printf("%+v\n",err)return}iferr!=nil{//unknownerror}}/*输出:datanotfound,Callfailed:GetSqfailed:sql:norowsinresultsetbarfailed:main.Call/usr/four/main.go:12-GetSqfailed:main.GetSql/usr/four/main.go:18-sql:norowsinresultset*/与github.com/pkg/errors相比它有一些缺点:使用:%w代替Wrap,看似简化,但失去了编译时检查。如果没有冒号,或者:%w不在格式化字符串的末尾,或者冒号和百分号之间没有空格,包装器将失败,不会报错;此外,在调用xerrors.Errorf之前,参数必须为nil。这根本没有简化开发人员的工作场景3:Go1.13内置支持Go1.13将xerrors的一些(但不是全部)功能集成到标准库中。它继承了上述xerrors的所有缺点,又贡献了一个。所以目前没有必要使用它。import("database/sql""errors""fmt")funcCall()error{iferr:=GetSql();err!=nil{returnfmt.Errorf("Callfailed:%w",GetSql())}returnnil}funcGetSql()error{returnfmt.Errorf("GetSqlfailed:%w",sql.ErrNoRows)}funcmain(){err:=Call()iferrors.Is(err,sql.ErrNoRows){fmt.Printf("datanotfound,%+v\n",err)return}iferr!=nil{//unknownerror}}/*Outputs:datanotfound,Callfailed:GetSqfailed:sql:norowsinresultset*/上面的代码非常接近xerrors版本。但是不支持调用栈信息的输出。根据官方说法,该功能目前还没有明确的支持时间。所以它的用处远不如github.com/pkg/errors。Golang未来可能的错误处理方式在Go2草案中,我们看到了一些与错误相关的提案,即check/handle函数。我们可能会在Golang的下一个主要版本中处理这样的错误:(){resource.Release()}()profile:=checkloadProfile()//returnprofile,errordeferfunc(){profile.Close()}//...}有兴趣的同学可以关注一下这个建议:https://go.googlesource.com/proposal/+/master/design/go2draft-error-handling-overview.md得出结论,重要的是要记住包装错误会使该错误成为API的一部分。如果您不想在将来支持将错误作为API的一部分,则不应包装错误。无论错误是否被包装,错误文本都是相同的。无论哪种方式,那些试图理解错误的人都会得到相同的信息;是否包装错误的选择取决于您是要为程序提供更多信息以便他们做出更明智的决定,还是保留该信息以保留抽象层。通过以上对比,相信你已经有了选择。只是为了阐明我的观点,如果您使用的是github.com/pkg/errors,请保持原样。目前没有比它更好的选择。如果你已经大量使用golang.org/x/xerrors,请不要盲目转而使用go1.13的内置解决方案。总的来说,Go从诞生之日起,在各方面都已经相当成熟和稳定。进化线上很少有犹豫和动摇,但错误处理是个例外。除了广受诟病的iferr!=nil之外,就连其改进路线也争议很大,分歧明显,以至于一个改进方案都会因反对声势浩大而不得不调整。幸运的是,Go团队比以前更愿意倾听社区的声音,团队甚至专门针对这个问题创建了一个反馈收集页面。相信大家最终都会找到更好的解决办法。
