个人对Go的错误处理有哪些不满,我是如何处理的方法 (个人对工作错误的态度与认识)
整理分享个人对Go的错误处理有哪些不满,我是如何处理的方法 (个人对工作错误的态度与认识),希望有所帮助,仅作参考,欢迎阅读内容。
内容相关其他词:对错误的认识及今后的改正,对于错误的认识和态度,个人错误的总结与改善措施,个人错误认识范文,对工作错误的认识,对工作错误的认识,个人的错误观念,对工作错误的认识,内容如对您有帮助,希望把内容链接给更多的朋友!
写Go的人往往对它的错误处理模式有一定的看法。按不一样的语言经验,人们可能有不一样的习惯处理方式。这就是为什么我决定要写这篇文章,尽管有点固执己见,但我认为听取我的经验是有用的。--AndrewMorgan本文导航-与其他语言的快速比较……%-实现集中式错误处理……%-错误上下文……%-堆栈*问题搞定方案……%-我们对错误应该做什么?……%-将失败的多条语句做为一个整体处理错误……%-总结……%编译自: 写Go的人往往对它的错误处理模式有一定的看法。按不一样的语言经验,人们可能有不一样的习惯处理方式。这就是为什么我决定要写这篇文章,尽管有点固执己见,但我认为听取我的经验是有用的。我想要讲的主要问题是,很难去强制执行良好的错误处理实践,错误经常没有堆栈定位,并且错误处理本身太冗长。不过,我已经看到了一些潜在的搞定方案,或许能帮助搞定一些问题。与其他语言的快速比较 在Go中,所有的错误都是值[1]。因为这点,相当多的函数最后会返回一个error,看起来像这样:func(s*SomeStruct)Function()(string,error) 因此这导致调用代码通常会运用if语句来检查它们:bytes,err:=someStruct.Function()iferr!=nil{//Processerror} 另外一种方式,是在其他语言中,如Java、C#、Javascript、ObjectiveC、Python等运用的try-catch模式。如下你可以看到与先前的Go示例相似的Java代码,声明throws而不是返回error:publicStringfunction()throwsException 它运用的是try-catch而不是iferr!=nil:try{Stringresult=someObject.function()//continuelogic}catch(Exceptione){//processexception} 当然,还有其他的不一样。例如,error不会使你的程序崩溃,然而Exception会。还有其他的一些,在本篇中会专门提到这些。实现集中式错误处理 退一步,让我们看看为什么要在一个集中的地方处理错误,以及如何做到。 大多数人或许会熟悉的一个例子是web服务-如果出现了一些未预料的的服务端错误,我们会生成一个5xx错误。在Go中,你或许会这么实现:funcinit(){http.HandleFunc("/users",viewUsers)http.HandleFunc("/companies",viewCompanies)}funcviewUsers(whttp.ResponseWriter,r*http.Request){user//somecodeiferr:=userTemplate.Execute(w,user);err!=nil{http.Error(w,err.Error(),)}}funcviewCompanies(whttp.ResponseWriter,r*http.Request){companies=//somecodeiferr:=companiesTemplate.Execute(w,companies);err!=nil{http.Error(w,err.Error(),)}} 这并不是一个好的搞定方案,因为我们不得不重复地在所有的处理函数中处理错误。为了能更好地维护,最好能在一处地方处理错误。幸运的是,在Go语言的官方博客中,AndrewGerrand提供了一个替代方式[2],可以完美地实现。我们可以创建一个处理错误的Type:typeappHandlerfunc(http.ResponseWriter,*http.Request)errorfunc(fnappHandler)ServeHTTP(whttp.ResponseWriter,r*http.Request){iferr:=fn(w,r);err!=nil{http.Error(w,err.Error(),)}} 这可以作为一个封装器来修饰我们的处理函数:funcinit(){http.Handle("/users",appHandler(viewUsers))http.Handle("/companies",appHandler(viewCompanies))} 接着我们需要做的是修改处理函数的签名来使它们返回errors。这个方式很好,因为我们做到了DRY[3]原则,并且没有重复运用不必要的代码-现在我们可以在单独一个地方返回默认错误了。错误上下文 在先前的例子中,我们可能会收到许多潜在的错误,它们中的任何一个都可能在调用堆栈的许多环节中生成。这时候事情就变得棘手了。 为了演示这点,我们可以扩展我们的处理函数。它可能看上去像这样,因为模板执行并不是唯一一处会发生错误的地方:funcviewUsers(whttp.ResponseWriter,r*http.Request)error{user,err:=findUser(r.formValue("id"))iferr!=nil{returnerr;}returnuserTemplate.Execute(w,user);} 调用链可能会相当深,在整个过程中,各种错误可能在不一样的地方实例化。RussCox[4]的这篇文章解释了如何避免遇到太多这类问题的最佳实践:“在Go中错误报告的部分约定是函数包含相关的上下文,包括正在尝试的*作(比如函数名和它的参数)。” 这个给出的例子是对OS包的一个调用:err:=os.Remove("/tmp/nonexist")fmt.Println(err) 它会输出:remove/tmp/nonexist:nosuchfileordirectory 总结一下,执行后,输出的是被调用的函数、给定的参数、特定的出错信息。当在其他语言中创建一个Exception消息时,你也可以遵循这个实践。如果我们在viewUsers处理中坚持这点,那么几乎总是能明确错误的原因。 问题来自于那些不遵循这个最佳实践的人,并且你经常会在第三方的Go库中看到这些消息:OhnoIbroke 这没什么帮助-你无法了解上下文,这使得调试很困难。更糟糕的是,当这些错误被忽视或返回时,这些错误会被备份到堆栈中,直到它们被处理为止:iferr!=nil{returnerr} 这意味着错误何时发生并没有被传递出来。 应该注意的是,所有这些错误都可以在Exception驱动的模型中发生-糟糕的错误信息、隐藏异常等。那么为什么我认为该模型更有用? 即便我们在处理一个糟糕的异常消息,我们依旧能够了解它发生在调用堆栈中什么地方。因为堆栈*,这引发了一些我对Go不了解的部分-你知道Go的panic包含了堆栈*,但是error没有。我推测可能是panic会使你的程序崩溃,因此需要一个堆栈*,而处理错误并不会,因为它会假定你在它发生的地方做一些事。 所以让我们回到之前的例子-一个有糟糕错误信息的第三方库,它只是输出了调用链。你认为调试会更容易吗?panic:OhnoIbroke[signal0xbcode=0x1addr=0x0pc=0xfcf]goroutine[running]:panic(0x4bed,0xcc0b0)/usr/local/go/src/runtime/panic.go:+0x3e6github*/Org/app/core.(_app).captureRequest(0xc,0x0,0xbd,0x0,0x0)/home/ubuntu/.go_workspace/src/github*/Org/App/core/main.go:+0xcfgithub*/Org/app/core.(_app).processRequest(0xc,0xce1c0,0xcaab8,0x1)/home/ubuntu/.go_workspace/src/github*/Org/App/core/main.go:+0xb6github*/Org/app/core.NewProxy.func2(0xce1c0,0xcbb,0xcbb,0x1)/home/ubuntu/.go_workspace/src/github*/Org/App/core/proxy.go:+0x2agithub*/Org/app/core/vendor/github*/rusenask/goproxy.FuncReqHandler.Handle(0xcdae0,0xce1c0,0xcbb,0xc,0xcb4a0a0)/home/ubuntu/.go_workspace/src/github*/Org/app/core/vendor/github*/rusenask/goproxy/actions.go:+0x 我认为这可能是Go的规划中被忽视的东西-不是所有语言都不会忽视的。 如果我们运用Java作为一个随意的例子,其中人们犯的一个最愚蠢的错误是不记录堆栈定位:LOGGER.error(ex.getMessage())//不记录堆栈*LOGGER.error(ex.getMessage(),ex)//记录堆栈定位 但是Go似乎在规划中就没有这个信息。 在获得上下文信息方面-Russ还提到了社区正在讨论一些潜在的接口用于剥离上下文错误。关于这点,了解更多或许会很有趣。堆栈*问题搞定方案 幸运的是,在做了一些查找后,我发现了这个出色的Go错误[5]库来帮助搞定这个问题,来给错误添加堆栈*:iferrors.Is(err,crashy.Crashed){fmt.Println(err.(*errors.Error).ErrorStack())} 不过,我认为这个功能如果能成为语言的第一类公民firstclasscitizenship将是一个改进,这样你就不必做一些类型修改了。此外,如果我们像先前的例子那样运用第三方库,它可能没有运用crashy-我们仍有相同的问题。我们对错误应该做什么? 我们还必须考虑发生错误时应该发生什么。这一定有用,它们不会让你的程序崩溃[6],通常也会立即处理它们:err:=method()iferr!=nil{//somelogicthatImustdonowintheeventofanerror!} 如果我们想要调用大量方式,它们会产生错误,然后在一个地方处理所有错误,这时会发生什么?看上去像这样:err:=doSomething()iferr!=nil{//handletheerrorhere}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博客中[7]发现了一个比较务实的搞定方案。 他定义了一个封装了错误的方式的结构体: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])//andsoonifew.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检查带来了更多的冗余。如果有人能提供其他搞定方案,我会很有兴趣听。或许这个语言本身需要一些方式来以不那么臃肿的方式传递或者组合错误-但是感觉似乎是特意规划成不那么做。总结 看完这些之后,你可能会认为我在对error挑刺儿,由此推论我反对Go。事实并非如此,我只是将它与我运用trycatch模型的经验进行比较。它是一个用于*编程很好的语言,并且已经出现了一些优秀的工具。仅举几例,有Kubernetes[8]、Docker[9]、Terraform[]、Hoverfly[]等。还有小型、高性能、本地二进制的优点。但是,error难以适应。我希望我的推论是有道理的,而且一些方案和搞定方式可能会有帮助。[1]:在Go中,所有的错误都是值-