不想Go 錯誤處理太臃腫,可以參考這個代碼設(shè)計
最近寫了個程序,因為是急活(貌似沒有不急的...),所以這個程序又是我東拷一段,西粘一塊拼出來的。代碼寫完了后,感覺這代碼屎一樣,都快把自己看哭了。真的是在心里邊寫別罵,先是罵以前做這個項目的人蠢,項目搞的跟屎一樣,后來代碼跑起來了,順利交工后,變成了罵我自己蠢,這么寫又不是不能用!
又不是不能用
不過在這個過程中,先不提項目里的業(yè)務(wù)邏輯、接口設(shè)計合不合理的事兒,這個我覺得在時間緊,加上人員更迭快的時候,正常人都會能粘就粘,不行了就再包一層,別改出線上問題了就行。有一點我把自己蠢哭的是,Go 的這個錯誤處理也太TM蠢了,一個程序我寫了七八個錯誤判斷,我給你們用偽代碼描述一下:
err, file := 接收傳文件(文件)
if err != nil {
記日志
返回錯誤碼相應(yīng)
}
err, fh := 打開上傳文件(file)
if err != nil {
記日志
返回錯誤碼相應(yīng)
}
err, data := 把文件里的行記錄解析/轉(zhuǎn)換一下(row)
if err != nil {
記日志
返回錯誤碼相應(yīng)
}
err, data3 := 調(diào)一下第三方接口拿數(shù)據(jù)
if err != nil {
記日志
返回錯誤碼相應(yīng)
}
err, data2 := 調(diào)一下內(nèi)部其他服務(wù)拿數(shù)據(jù)
if err != nil {
記日志
返回錯誤碼相應(yīng)
}
err := 寫庫
if err != nil {
記日志
返回錯誤碼相應(yīng)
}
上面這個例子毫不夸張,我相信各位在自己的項目里一定見過,如果你是做業(yè)務(wù)開發(fā)的會更常見。
這里有人肯定會問,Go的錯誤處理就這樣你難道第一天見嗎,還能被蠢哭。誒,這不是降本提效后人員少了一半,我們這幫級別沒混上去的虛線Leader,這不又開始自己寫代碼了嘛,以前蠢又蠢不到自己。再加上以前的系統(tǒng)、項目分層、服務(wù)隔離整的還湊活,不會像上面這樣,在控制層調(diào)這么多業(yè)務(wù)對象,把蠢瓜代碼集中在了一起…… 官感馬上不一樣了。
于是乎我就在思考,有沒有什么設(shè)計模式什么的,能把這些東西隱藏下去,應(yīng)該有吧,沒有什么是包一層代碼解決不了的吧,實在不行就包兩層……誒,咋一不小心把設(shè)計模式的精髓給說出來了。
Go 優(yōu)雅處理錯誤的幾種方案
我這幾天在網(wǎng)上看了不少說,Go 錯誤處理的,但基本上都是說怎么自定義包裝 error 、傳遞error 之類的,講怎么在寫 Go 代碼時能更優(yōu)雅更好看的文章比較少,寫的最好的是左耳朵耗子老師在自己博客里介紹的兩種方式。
一種是用函數(shù)式編程的 Closure 把相同的 if err !=nil 之類的代碼抽象出來重新定義一個函數(shù),但是這種方式會導(dǎo)致新的問題--在每個函數(shù)里都需要引入內(nèi)部函數(shù)和一個 error 變量,所以咱就不多說了,有興趣的可以去原博文查看。
這里直接介紹另外一種更好的,對項目侵入不是很大的方案給大家。在 Go 語言官方庫 bufio? 中 Scanner對象的錯處理的實現(xiàn)方式可以給我們一點啟發(fā),它大概是這么實現(xiàn)的。
scanner := bufio.NewScanner(input)
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
上面的代碼我們可以看到,scanner?在操作底層的I/O的時候,那個for-loop中沒有任何的 if err !=nil? 的情況,退出循環(huán)后有一個 scanner.Err() 的檢查??磥硎褂昧私Y(jié)構(gòu)體的方式。
我們來看一下 Scanner類型的定義:
type Scanner struct {
r io.Reader
...//其他字段省略
err error
}
這個類型內(nèi)部持有一個error 在迭代執(zhí)行 Scan 方法時,遇到錯誤后會往這個 error 中記錄錯誤。
func (s *Scanner) Scan() bool {
...// 其余代碼省略
for {
if err != nil {
s.setErr(err)
return false
}
}
func (s *Scanner) Err() error {
if s.err == io.EOF {
return nil
}
return s.err
}
所以我們可以參考這個思路繼續(xù)搞下去。比如來一個讀取業(yè)務(wù)對象的
上面這個示例相信大家很容易看懂,不過,其使用場景也就只能在對于同一個業(yè)務(wù)對象的不斷操作下可以簡化錯誤處理,對于多個業(yè)務(wù)對象的話,還是得需要各種 if err != nil的方式。
那有什么辦法呢,咱們之前說過一次:沒有什么是包一層代碼解決不了的吧,實在不行就包兩層。那么接下來我們再做一層包裝,以下是我對解決這個問題的一點點理解,會借鑒一點DDD中分層的概念解決這個事情。
更容易落地的方案
剛才那個例子的問題是只適合減少單個業(yè)務(wù)對象邏輯操作中的 if err != nill 判斷,那么針對這塊呢,咱們可以把涉及多個業(yè)務(wù)對象的操作放在一個應(yīng)用服務(wù)里,把剛才在業(yè)務(wù)對象做的錯誤處理判斷拿到應(yīng)用服務(wù)里,這樣業(yè)務(wù)對象里,比如Model之類的下層模塊里,就還能按照正常的流程寫代碼了,不用每個方法開頭都要先判斷一下。
這里提前說一下,在一些架構(gòu)設(shè)計里會分應(yīng)用服務(wù)和領(lǐng)域服務(wù),這兩者的概念完全不一樣,應(yīng)用服務(wù)是面向產(chǎn)品需求的用例實現(xiàn)的,負責(zé)業(yè)務(wù)用例流的任務(wù)協(xié)調(diào),就是我們實現(xiàn)API時,往往會控制層調(diào)應(yīng)用服務(wù),多個不同的業(yè)務(wù)對象可以放到一個應(yīng)用服務(wù)里。而領(lǐng)域服務(wù)是專一給一個領(lǐng)域的,這塊我就不多解釋了,DDD這些我也是看了幾本書,看過COLA框架的實現(xiàn),還在似懂非懂的水平。
總之記住一點,通過應(yīng)用服務(wù)可以協(xié)調(diào)多個業(yè)務(wù)對象執(zhí)行任務(wù),同時我們上面業(yè)務(wù)對象加的那些錯誤處理抽離到應(yīng)用服務(wù)層里,讓業(yè)務(wù)對象更專注自己的職責(zé)。這樣的話,你的服務(wù)層代碼,可能就得變成了這樣
然后我們的控制層呢,調(diào)用應(yīng)用服務(wù)層拿到結(jié)果,并且在這個時候判斷整個需求任務(wù)執(zhí)行的過程中有沒有錯誤,有的話記錄錯誤,返回錯誤響應(yīng)給客戶端。
Go 錯誤處理的基礎(chǔ)
之前分享過一篇文章??關(guān)于Go程序錯誤處理的一些建議??說的是我們應(yīng)該怎么用好 Go 的error 接口,自定義錯誤,包裝整個錯誤鏈等相關(guān)的技能。跟本文的內(nèi)容關(guān)聯(lián)起來看,可能會對錯誤處理有個更全局的理解,在這里也推薦給大家。
總結(jié)
今天給大家分享了一些在讓Go代碼的錯誤處理更優(yōu)雅上,我學(xué)到和?想到的一些東西。其實大家可以發(fā)現(xiàn),我們是把多個 if err != nil 分散到了多個方法里,這樣代碼最起碼從感官上看起來比在一個方法里寫七八個錯誤判斷更好一點。