Golang中重复错误处理的优化方法
Golang错误处理最让人头疼的问题就是代码里充斥着「iferr!=nil」,它们破坏了代码的可读性,本文收集了几个例子,让大家明白如何优化此类问题。
让我们看看Errorsarevalues中提到的一个io.Writer例子:
_,err=fd.Write(p0[a:b]) iferr!=nil{ returnerr } _,err=fd.Write(p1[c:d]) iferr!=nil{ returnerr } _,err=fd.Write(p2[e:f]) iferr!=nil{ returnerr }
如上代码乍一看无法直观的看出其本来的意图是什么,改进版:
typeerrWriterstruct{ wio.Writer errerror } 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 }
通过自定义类型errWriter来封装io.Writer,并且封装了error,新类型有一个write方法,不过其方法签名并没有返回error,而是在方法内部判断一旦有问题就立刻返回,有了这些准备工作,我们就可以把原本穿插在业务逻辑中间的错误判断提出来放到最后来统一调用,从而在视觉上保证让人可以直观的看出代码本来的意图是什么。
让我们再看看Eliminateerrorhandlingbyeliminatingerrors中提到的另一个io.Writer例子:
typeHeaderstruct{ Key,Valuestring } typeStatusstruct{ Codeint Reasonstring } funcWriteResponse(wio.Writer,stStatus,headers[]Header,bodyio.Reader)error{ _,err:=fmt.Fprintf(w,"HTTP/1.1%d%s\r\n",st.Code,st.Reason) iferr!=nil{ returnerr } for_,h:=rangeheaders{ _,err:=fmt.Fprintf(w,"%s:%s\r\n",h.Key,h.Value) iferr!=nil{ returnerr } } if_,err:=fmt.Fprint(w,"\r\n");err!=nil{ returnerr } _,err=io.Copy(w,body) returnerr }
第一感觉既然错误是fmt.Fprint和io.Copy返回的,是不是我们要重新封装一下它们?实际上真正的源头是它们的参数io.Writer,因为直接调用io.Writer的Writer方法的话,方法签名中有返回值error,所以每一步fmt.Fprint和io.Copy操作都不得不进行重复的错误处理,看上去是坏味道,改进版:
typeerrWriterstruct{ io.Writer errerror } func(e*errWriter)Write(buf[]byte)(int,error){ ife.err!=nil{ return0,e.err } varnint n,e.err=e.Writer.Write(buf) returnn,nil } funcWriteResponse(wio.Writer,stStatus,headers[]Header,bodyio.Reader)error{ ew:=&errWriter{Writer:w} fmt.Fprintf(ew,"HTTP/1.1%d%s\r\n",st.Code,st.Reason) for_,h:=rangeheaders{ fmt.Fprintf(ew,"%s:%s\r\n",h.Key,h.Value) } fmt.Fprint(ew,"\r\n") io.Copy(ew,body) returnew.err }
通过自定义类型errWriter来封装io.Writer,并且封装了error,同时重写了Writer方法,虽然方法签名中仍然有返回值error,但是我们单独保存了一份error,并且在方法内部判断一旦有问题就立刻返回,有了这些准备工作,新版的WriteResponse不再有重复的错误判断,只需要在最后检查一下error即可。
类似的做法在Golang标准库中屡见不鲜,让我们继续看看Eliminateerrorhandlingbyeliminatingerrors中提到的一个关于bufio.Reader和bufio.Scanner的例子:
funcCountLines(rio.Reader)(int,error){ var( br=bufio.NewReader(r) linesint errerror ) for{ _,err=br.ReadString('\n') lines++ iferr!=nil{ break } } iferr!=io.EOF{ return0,err } returnlines,nil }
我们构造一个bufio.Reader,然后在一个循环中调用ReadString方法,如果读到文件结尾,那么ReadString会返回一个错误(io.EOF),为了判断此类情况,我们不得不在每次循环时判断「iferr!=nil」,看上去这是坏味道,改进版:
funcCountLines(rio.Reader)(int,error){ sc:=bufio.NewScanner(r) lines:=0 forsc.Scan(){ lines++ } returnlines,sc.Err() }
实际上,和bufio.Reader相比,bufio.Scanner是一个更高阶的类型,换句话简单点来说的话,相当于是bufio.Scanner抽象了bufio.Reader,通过把低阶的bufio.Reader换成高阶的bufio.Scanner,循环中不再需要判断「iferr!=nil」,因为Scan方法签名不再返回error,而是返回bool,当在循环里读到了文件结尾的时候,循环直接结束,如此一来,我们就可以统一在最后调用Err方法来判断成功还是失败,看看Scanner的定义:
typeScannerstruct{ rio.Reader//Thereaderprovidedbytheclient. splitSplitFunc//Thefunctiontosplitthetokens. maxTokenSizeint//Maximumsizeofatoken;modifiedbytests. token[]byte//Lasttokenreturnedbysplit. buf[]byte//Bufferusedasargumenttosplit. startint//Firstnon-processedbyteinbuf. endint//Endofdatainbuf. errerror//Stickyerror. emptiesint//Countofsuccessiveemptytokens. scanCalledbool//Scanhasbeencalled;bufferisinuse. donebool//Scanhasfinished. }
可见Scanner封装了io.Reader,并且封装了error,和我们之前讨论的做法一致。有一点说明一下,实际上查看Scan源代码的话,你会发现它不是通过err来判断是否结束的,而是通过done来判断是否结束,这是因为Scan只有遇到文件结束的错误才退出,其它错误会继续执行,当然,这只是具体的细节问题,不影响我们的结论。
通过对以上几个例子的分析,我们可以得出优化重复错误处理的大概套路:通过创建新的类型来封装原本干脏活累活的旧类型,同时在新类型中封装error,新旧类型的方法签名可以保持兼容,也可以不兼容,这个不是关键的,视客观情况而定,至于具体的逻辑实现,先判断有没有error,如果有就直接退出,如果没有就继续执行,并且在执行过程中保存可能出现的error以便后面操作使用,最后通过统一调用新类型的error来完成错误处理。提醒一下,此方案的缺点是要到最后才能知道有没有错误,好在如此的控制粒度在多数时候并无大碍。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对毛票票的支持。