写Go的人往往对其错误处理模型有一定的看法。根据不同的语言经历,人们可能会有不同的习惯处理方式。这就是我决定写这篇文章的原因,虽然我有点自以为是,但我认为听听我的经验会很有用。我想解决的主要问题是很难实施良好的错误处理实践,错误通常没有堆栈跟踪,而且错误处理本身过于冗长。但是,我看到了一些可能有助于解决某些问题的潜在解决方案。与其他语言的快速比较在Go中,所有错误都是值。因此,相当多的函数***返回一个错误,看起来像这样:func(s*SomeStruct)Function()(string,error)所以这导致调用代码通常使用if语句来检查它们:bytes,err:=someStruct.Function()iferr!=nil{//Processerror}另一种方法是其他语言使用的try-catch模式,比如Java、C#、Javascript、ObjectiveC、Python等,下面你可以看到Java代码类似于前面的Go示例,声明throws而不是返回错误:publicStringfunction()throwsException它使用了try-catch而不是iferr!=nil:try{Stringresult=someObject.function()//continuelogic}catch(Exceptione){//处理异常}当然还有其他的区别。例如,error不会使您的程序崩溃,但Exception会。还有其他的,本文会专门提到。实施集中式错误处理退后一步,让我们看看为什么以及如何在一个集中的地方处理错误。大多数人可能熟悉的一个例子是Web服务——如果出现一些意外的服务器端错误,我们会生成5xx错误。在Go中,你可以这样实现它:Request){user//一些代码iferr:=userTemplate.Execute(w,user);err!=nil{http.Error(w,err.Error(),500)}}funcviewCompanies(whttp.ResponseWriter,r*http.Request){companies=//一些代码iferr:=companiesTemplate.Execute(w,公司);err!=nil{http.Error(w,err.Error(),500)}}这不是一个好的解决方案,因为我们必须在所有处理程序中重复处理错误。为了更好的维护,***可以在一处处理错误。幸运的是,在Go语言的官方博客中,AndrewGerrand提供了一种可以安全实现的替代方法。我们可以创建一个处理错误的类型:);err!=nil{http.Error(w,err.Error(),500)}}这可以用作修改我们的处理函数的包装器:funcinit(){http.Handle("/users",appHandler(viewUsers))http.Handle("/companies",appHandler(viewCompanies))}接下来我们需要做的是修改处理函数的签名,以便它们返回错误。这种方法很好,因为我们是DRY并且不重用不必要的代码-现在我们可以在一个地方返回默认错误。错误上下文在前面的示例中,我们可能已经收到许多潜在的错误,其中任何错误都可能在调用堆栈中的许多点生成。这就是事情变得棘手的时候。为了证明这一点,我们可以扩展我们的处理函数。它可能看起来像这样,因为模板执行不是唯一可能发生错误的地方:funcviewUsers(whttp.ResponseWriter,r*http.Request)error{user,err:=findUser(r.formValue("id"))iferr!=nil{返回错误;}returnuserTemplate.Execute(w,user);}调用链可能很深,一路上可能会在不同的地方实例化各种错误。RussCox的这篇文章解释了如何避免遇到太多此类问题的最佳实践:“Go中错误报告的部分约定是函数包含相关上下文,包括正在尝试的操作(例如函数名称和它的参数)。“此处给出的示例是对操作系统包的调用:err:=os.Remove("/tmp/nonexist")fmt.Println(err)将输出:remove/tmp/nonexist:nosuchfileordirectory之后的摘要一段时间后,执行后,输出的是被调用的函数,给定的参数,以及具体的错误信息,其他语言创建Exceptionmessage也可以照此做法,如果我们在viewUsers处理中硬要这样,那么原因错误几乎总是很清楚。问题来自不遵循此最佳实践的人,你会经常在第三方Go库中看到这些消息:哦不,我坏了这没有帮助-你没有办法知道上下文,这使得调试变得困难。更糟糕的是,当这些错误被忽略或返回时,它们会在堆栈中备份,直到它们被处理:iferr!=nil{returnerr}这意味着错误发生并且不会传播.需要注意的是,所有这些错误都可能发生在Exceptiondrivenmodel-糟糕的错误消息、隐藏的异常等。那么为什么我认为这个模型更有用呢?即使我们在处理错误的异常消息中,我们仍然能够了解它在调用堆栈中发生的位置。由于堆栈跟踪,这引发了一些我不知道的关于Go的部分——你知道Go的恐慌包括堆栈跟踪,但错误没有。我推测恐慌可能会使您的程序崩溃,因此需要堆栈跟踪,而处理错误则不需要,因为它会假设您在发生错误的地方做正确的事情。那么让我们回到我们之前的例子——一个堆栈跟踪,带有可怕的错误消息第三方库,它只打印调用链。您认为调试会更容易吗?恐慌:哦,不,我打破了[信号0xb代码=0x1地址=0x0pc=0xfc90f]goroutine1103[运行]:恐慌(0x4bed00,0xc82000c0b0)/usr/local/go/src/runtime/panic.go:481+0x3e6github。com/Org/app/core.(_app).captureRequest(0xc820163340,0x0,0x55bd50,0x0,0x0)/home/ubuntu/.go_workspace/src/github.com/Org/App/core/main.go:313+0x12cfgithub.com/Org/app/core.(_app).processRequest(0xc820163340,0xc82064e1c0,0xc82002aab8,0x1)/home/ubuntu/.go_workspace/src/github.com/Org/App/core/main.go:203+0xb6github.com/Org/app/core.NewProxy.func2(0xc82064e1c0,0xc820bb2000,0xc820bb2000,0x1)/home/ubuntu/.go_workspace/src/github.com/Org/App/core/proxy.go:51+0x2agithub.com/Org/app/core/vendor/github.com/ruse??nask/goproxy.FuncReqHandler.Handle(0xc820da36e0,0xc82064e1c0,0xc820bb2000,0xc5001,0xc820b4a0a0)/home/ubuntu/space/github.com/Org/app/core/vendor/github.com/ruse??nask/goproxy/actions.go:19+0x30我认为这可能是Go的设计中被忽略的东西——并非所有语言都没有。如果我们以Java为例,人们犯的最愚蠢的错误之一就是不记录堆栈跟踪:LOGGER.error(ex.getMessage())//不记录堆栈跟踪LOGGER.error(ex.getMessage(),ex)//记录堆栈跟踪,但Go似乎没有设计为具有此信息。在获取上下文信息方面——Russ还提到社区正在讨论一些潜在的接口来去除上下文错误。了解更多这方面的信息可能会很有趣。堆栈跟踪问题解决方案幸运的是,在进行一些搜索之后,我发现了这个优秀的Go错误库来帮助解决这个问题,将堆栈跟踪添加到错误中:??iferrors.Is(err,crashy.Crashed){fmt.Println(err.(*errors.Error).ErrorStack())}但是,我认为将此功能作为该语言的一等公民将是一个改进,这样您就不必进行一些类型修改。另外,如果我们像前面的例子一样使用第三方库,它可能不会崩溃——我们仍然有同样的问题。我们应该如何处理错误?我们还必须考虑发生错误时应该发生什么。这必须有效,它们不会使您的程序崩溃,并且它们通常会立即得到处理:err:=method()iferr!=nil{//如果出现错误,我现在必须做的一些逻辑!}if当我们想调用很多产生错误的方法并在一个地方处理它们时会发生什么?看起来像这样:err:=doSomething()iferr!=nil{//在这里处理错误}funcdoSomething()error{err:=someMethod()iferr!=nil{returnerr}err=someOther()iferr!=nil{returnerr}someOtherMethod()}这感觉有点多余,在其他语言中你可以将多个语句作为一个处理。try{someMethod()someOther()someOtherMethod()}catch(Exceptione){//processexception}或者只是在方法签名中传递错误:publicvoiddoSomething()throwsSomeErrorToPropogate{someMethod()someOther()someOtherMethod()}我个人认为这两个例子实现的是一件事,但是Exception模式的冗余少,更加灵活。如果有的话,iferr!=nil对我来说就像样板文件。也许有办法清理它?将失败的多个语句作为单个错误处理首先,我做了更多阅读,并在RobPike的Go博客上找到了一个更实用的解决方案。他定义了一个封装错误方法的结构体:typeerrWriterstruct{wio.Writererrerror}func(ew*errWriter)write(buf[]byte){ifew.err!=nil{return}_,ew.err=ew.w.Write(buf)}让我们这样做:ew:=&errWriter{w:fd}ew.write(p0[a:b])ew.write(p1[c:d])ew.write(p2[e:f])//等等ifew.err!=nil{returnew.err}这也是一个很好的解决方案,但我觉得缺少了一些东西——因为我们不能重用该模式。如果我们想要一个带有字符串参数的方法,我们将不得不更改函数签名。或者我们不想执行写操作怎么办?我们可以尝试让它更通用:typeerrWrapperstruct{errerror}func(ew*errWrapper)do(ffunc()error){ifew.err!=nil{return}ew.err=f();}但是我们有同样的问题,如果我们尝试用不同的参数调用一个函数,它不会编译。但是,您可以简单地包装这些函数调用:w:=&errWrapper{}w.do(func()error{returnsomeFunction(1,2);})w.do(func()error{returnotherFunction("foo");})err:=w.erriferr!=nil{//processerrorhere}这行得通,但帮助不大,因为它最终会带来比标准iferr!=nil检查冗余更多的错误。如果有人可以提供另一种解决方案,我会很想听听。也许语言本身需要某种方式来以不那么臃肿的方式传递或组合错误——但感觉它是故意设计的,而不是这样做的。总结阅读本文后,您可能认为我在挑剔,推而广之,我反对Go。事实并非如此,我只是将其与我使用trycatch模型的经验进行比较。它是一种出色的系统编程语言,并且已经出现了一些出色的工具。举几个例子,有Kubernetes、Docker、Terraform、Hoverfly等,也有体积小、性能高、原生二进制等优点。但是,错误很难适应。我希望我的推理是有道理的,一些场景和解决方法可能会有所帮助。
