Golang中重復(fù)錯(cuò)誤處理的優(yōu)化方法
Golang 錯(cuò)誤處理最讓人頭疼的問題就是代碼里充斥著「if err != nil」,它們破壞了代碼的可讀性,本文收集了幾個(gè)例子,讓大家明白如何優(yōu)化此類問題。
讓我們看看 Errors are values 中提到的一個(gè) io.Writer 例子:
_, err = fd.Write(p0[a:b]) if err != nil { return err } _, err = fd.Write(p1[c:d]) if err != nil { return err } _, err = fd.Write(p2[e:f]) if err != nil { return err }
如上代碼乍一看無法直觀的看出其本來的意圖是什么,改進(jìn)版:
type errWriter struct { w io.Writer err error } func (ew *errWriter) write(buf []byte) { if ew.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]) if ew.err != nil { return ew.err }
通過自定義類型 errWriter 來封裝 io.Writer,并且封裝了 error,新類型有一個(gè) write 方法,不過其方法簽名并沒有返回 error,而是在方法內(nèi)部判斷一旦有問題就立刻返回,有了這些準(zhǔn)備工作,我們就可以把原本穿插在業(yè)務(wù)邏輯中間的錯(cuò)誤判斷提出來放到最后來統(tǒng)一調(diào)用,從而在視覺上保證讓人可以直觀的看出代碼本來的意圖是什么。
讓我們?cè)倏纯?Eliminate error handling by eliminating errors 中提到的另一個(gè) io.Writer 例子:
type Header struct { Key, Value string } type Status struct { Code int Reason string } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) if err != nil { return err } for _, h := range headers { _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value) if err != nil { return err } } if _, err := fmt.Fprint(w, "\r\n"); err != nil { return err } _, err = io.Copy(w, body) return err }
第一感覺既然錯(cuò)誤是 fmt.Fprint 和 io.Copy 返回的,是不是我們要重新封裝一下它們?實(shí)際上真正的源頭是它們的參數(shù) io.Writer,因?yàn)橹苯诱{(diào)用 io.Writer 的 Writer 方法的話,方法簽名中有返回值 error,所以每一步 fmt.Fprint 和 io.Copy 操作都不得不進(jìn)行重復(fù)的錯(cuò)誤處理,看上去是壞味道,改進(jìn)版:
type errWriter struct { io.Writer err error } func (e *errWriter) Write(buf []byte) (int, error) { if e.err != nil { return 0, e.err } var n int n, e.err = e.Writer.Write(buf) return n, nil } func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error { ew := &errWriter{Writer: w} fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason) for _, h := range headers { fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value) } fmt.Fprint(ew, "\r\n") io.Copy(ew, body) return ew.err }
通過自定義類型 errWriter 來封裝 io.Writer,并且封裝了 error,同時(shí)重寫了 Writer 方法,雖然方法簽名中仍然有返回值 error,但是我們單獨(dú)保存了一份 error,并且在方法內(nèi)部判斷一旦有問題就立刻返回,有了這些準(zhǔn)備工作,新版的 WriteResponse 不再有重復(fù)的錯(cuò)誤判斷,只需要在最后檢查一下 error 即可。
類似的做法在 Golang 標(biāo)準(zhǔn)庫中屢見不鮮,讓我們繼續(xù)看看 Eliminate error handling by eliminating errors 中提到的一個(gè)關(guān)于 bufio.Reader 和 bufio.Scanner 的例子:
func CountLines(r io.Reader) (int, error) { var ( br = bufio.NewReader(r) lines int err error ) for { _, err = br.ReadString('\n') lines++ if err != nil { break } } if err != io.EOF { return 0, err } return lines, nil }
我們構(gòu)造一個(gè) bufio.Reader,然后在一個(gè)循環(huán)中調(diào)用 ReadString 方法,如果讀到文件結(jié)尾,那么 ReadString 會(huì)返回一個(gè)錯(cuò)誤(io.EOF),為了判斷此類情況,我們不得不在每次循環(huán)時(shí)判斷「if err != nil」,看上去這是壞味道,改進(jìn)版:
func CountLines(r io.Reader) (int, error) { sc := bufio.NewScanner(r) lines := 0 for sc.Scan() { lines++ } return lines, sc.Err() }
實(shí)際上,和 bufio.Reader 相比,bufio.Scanner 是一個(gè)更高階的類型,換句話簡(jiǎn)單點(diǎn)來說的話,相當(dāng)于是 bufio.Scanner 抽象了 bufio.Reader,通過把低階的 bufio.Reader 換成高階的 bufio.Scanner,循環(huán)中不再需要判斷「if err != nil」,因?yàn)?Scan 方法簽名不再返回 error,而是返回 bool,當(dāng)在循環(huán)里讀到了文件結(jié)尾的時(shí)候,循環(huán)直接結(jié)束,如此一來,我們就可以統(tǒng)一在最后調(diào)用 Err 方法來判斷成功還是失敗,看看 Scanner 的定義:
type Scanner struct { r io.Reader // The reader provided by the client. split SplitFunc // The function to split the tokens. maxTokenSize int // Maximum size of a token; modified by tests. token []byte // Last token returned by split. buf []byte // Buffer used as argument to split. start int // First non-processed byte in buf. end int // End of data in buf. err error // Sticky error. empties int // Count of successive empty tokens. scanCalled bool // Scan has been called; buffer is in use. done bool // Scan has finished. }
可見 Scanner 封裝了 io.Reader,并且封裝了 error,和我們之前討論的做法一致。有一點(diǎn)說明一下,實(shí)際上查看 Scan 源代碼的話,你會(huì)發(fā)現(xiàn)它不是通過 err 來判斷是否結(jié)束的,而是通過 done 來判斷是否結(jié)束,這是因?yàn)?Scan 只有遇到文件結(jié)束的錯(cuò)誤才退出,其它錯(cuò)誤會(huì)繼續(xù)執(zhí)行,當(dāng)然,這只是具體的細(xì)節(jié)問題,不影響我們的結(jié)論。
通過對(duì)以上幾個(gè)例子的分析,我們可以得出優(yōu)化重復(fù)錯(cuò)誤處理的大概套路:通過創(chuàng)建新的類型來封裝原本干臟活累活的舊類型,同時(shí)在新類型中封裝 error,新舊類型的方法簽名可以保持兼容,也可以不兼容,這個(gè)不是關(guān)鍵的,視客觀情況而定,至于具體的邏輯實(shí)現(xiàn),先判斷有沒有 error,如果有就直接退出,如果沒有就繼續(xù)執(zhí)行,并且在執(zhí)行過程中保存可能出現(xiàn)的 error 以便后面操作使用,最后通過統(tǒng)一調(diào)用新類型的 error 來完成錯(cuò)誤處理。提醒一下,此方案的缺點(diǎn)是要到最后才能知道有沒有錯(cuò)誤,好在如此的控制粒度在多數(shù)時(shí)候并無大礙。
總結(jié)
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
go使用makefile腳本編譯應(yīng)用的方法小結(jié)
makefile可以看作是make工具的腳本文件, 而make主要用來處理一系列命令。常用的比如用來編譯和打包文件, 在C/C++的編譯打包中應(yīng)用最廣泛了,這篇文章主要介紹了go使用makefile腳本編譯應(yīng)用,需要的朋友可以參考下2022-08-08Go?實(shí)現(xiàn)?WebSockets之創(chuàng)建?WebSockets
這篇文章主要介紹了Go?實(shí)現(xiàn)?WebSockets之創(chuàng)建?WebSockets,文章主要探索?WebSockets,并簡(jiǎn)要介紹了它們的工作原理,并仔細(xì)研究了全雙工通信,想了解更多相關(guān)內(nèi)容的小伙伴可以參考一下2022-04-04goland中導(dǎo)包報(bào)紅和go mod問題
這篇文章主要介紹了goland中導(dǎo)包報(bào)紅和go mod問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-03-03淺談beego默認(rèn)處理靜態(tài)文件性能低下的問題
下面小編就為大家?guī)硪黄獪\談beego默認(rèn)處理靜態(tài)文件性能低下的問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-06-06Golang實(shí)現(xiàn)按行讀取文件的方法小結(jié)
按行讀取文件相較于一次性載入,有著很多優(yōu)勢(shì),如內(nèi)存效率高、處理速度快、實(shí)時(shí)性高等,本文主要介紹了Golang按行讀取文件的相關(guān)方法,希望對(duì)大家有所幫助2024-02-02Go語言開源庫實(shí)現(xiàn)Onvif協(xié)議客戶端設(shè)備搜索
這篇文章主要為大家介紹了Go語言O(shè)nvif協(xié)議客戶端設(shè)備搜索示例實(shí)現(xiàn),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-04-04