Golang錯誤和異常處理的正確姿式

Golang錯誤和異常處理的正確姿式


  
  
  
  

錯誤和異常是兩個不一樣的概念,很是容易混淆。不少程序員習慣將一切非正常狀況都看作錯誤,而不區分錯誤和異常,即便程序中可能有異常拋出,也將異常及時捕獲並轉換成錯誤。從表面上看,一切皆錯誤的思路更簡單,而異常的引入僅僅增長了額外的複雜度。
但事實並不是如此。衆所周知,Golang遵循「少便是多」的設計哲學,追求簡潔優雅,就是說若是異常價值不大,就不會將異常加入到語言特性中。程序員

錯誤和異常處理是程序的重要組成部分,咱們先看看下面幾個問題:正則表達式

  1. 錯誤和異常如何區分?
  2. 錯誤處理的方式有哪幾種?
  3. 何時須要使用異常終止程序?
  4. 何時須要捕獲異常?
  5. ...

若是你對這幾個問題的答案不是太清楚,那麼就抽一點時間看看本文,或許能給你一些啓發。安全


face-to-exception.png

基礎知識

錯誤指的是可能出現問題的地方出現了問題,好比打開一個文件時失敗,這種狀況在人們的意料之中 ;而異常指的是不該該出現問題的地方出現了問題,好比引用了空指針,這種狀況在人們的意料以外。可見,錯誤是業務過程的一部分,而異常不是閉包

Golang中引入error接口類型做爲錯誤處理的標準模式,若是函數要返回錯誤,則返回值類型列表中確定包含error。error處理過程相似於C語言中的錯誤碼,可逐層返回,直到被處理。app

Golang中引入兩個內置函數panic和recover來觸發和終止異常處理流程,同時引入關鍵字defer來延遲執行defer後面的函數。
一直等到包含defer語句的函數執行完畢時,延遲函數(defer後的函數)纔會被執行,而無論包含defer語句的函數是經過return的正常結束,仍是因爲panic致使的異常結束。你能夠在一個函數中執行多條defer語句,它們的執行順序與聲明順序相反。
當程序運行時,若是遇到引用空指針、下標越界或顯式調用panic函數等狀況,則先觸發panic函數的執行,而後調用延遲函數。調用者繼續傳遞panic,所以該過程一直在調用棧中重複發生:函數中止執行,調用延遲執行函數等。若是一路在延遲函數中沒有recover函數的調用,則會到達該攜程的起點,該攜程結束,而後終止其餘全部攜程,包括主攜程(相似於C語言中的主線程,該攜程ID爲1)。函數

錯誤和異常從Golang機制上講,就是error和panic的區別。不少其餘語言也同樣,好比C++/Java,沒有error但有errno,沒有panic但有throw。學習

Golang錯誤和異常是能夠互相轉換的:測試

  1. 錯誤轉異常,好比程序邏輯上嘗試請求某個URL,最多嘗試三次,嘗試三次的過程當中請求失敗是錯誤,嘗試完第三次還不成功的話,失敗就被提高爲異常了。
  2. 異常轉錯誤,好比panic觸發的異常被recover恢復後,將返回值中error類型的變量進行賦值,以便上層函數繼續走錯誤處理流程。

一個啓示

regexp包中有兩個函數Compile和MustCompile,它們的聲明以下:ui



   
   
   
   

一樣的功能,不一樣的設計:編碼

  1. Compile函數基於錯誤處理設計,將正則表達式編譯成有效的可匹配格式,適用於用戶輸入場景。當用戶輸入的正則表達式不合法時,該函數會返回一個錯誤。
  2. MustCompile函數基於異常處理設計,適用於硬編碼場景。當調用者明確知道輸入不會引發函數錯誤時,要求調用者檢查這個錯誤是沒必要要和累贅的。咱們應該假設函數的輸入一直合法,當調用者輸入了不該該出現的輸入時,就觸發panic異常。

因而咱們獲得一個啓示:什麼狀況下用錯誤表達,什麼狀況下用異常表達,就得有一套規則,不然很容易出現一切皆錯誤或一切皆異常的狀況。

在這個啓示下,咱們給出異常處理的做用域(場景):

  1. 空指針引用
  2. 下標越界
  3. 除數爲0
  4. 不該該出現的分支,好比default
  5. 輸入不該該引發函數錯誤

其餘場景咱們使用錯誤處理,這使得咱們的函數接口很精煉。對於異常,咱們能夠選擇在一個合適的上游去recover,並打印堆棧信息,使得部署後的程序不會終止。

說明: Golang錯誤處理方式一直是不少人詬病的地方,有些人吐槽說一半的代碼都是"if err != nil { / 打印 && 錯誤處理 / }",嚴重影響正常的處理邏輯。當咱們區分錯誤和異常,根據規則設計函數,就會大大提升可讀性和可維護性。

錯誤處理的正確姿式

姿式一:失敗的緣由只有一個時,不使用error

咱們看一個案例:



   
   
   
   

咱們能夠看出,該函數失敗的緣由只有一個,因此返回值的類型應該爲bool,而不是error,重構一下代碼:



   
   
   
   

說明:大多數狀況,致使失敗的緣由不止一種,尤爲是對I/O操做而言,用戶須要瞭解更多的錯誤信息,這時的返回值類型再也不是簡單的bool,而是error。

姿式二:沒有失敗時,不使用error

error在Golang中是如此的流行,以致於不少人設計函數時無論三七二十一都使用error,即便沒有一個失敗緣由。
咱們看一下示例代碼:



   
   
   
   

對於上面的函數設計,就會有下面的調用代碼:



   
   
   
   

根據咱們的正確姿式,重構一下代碼:



   
   
   
   

因而調用代碼變爲:



   
   
   
   

姿式三:error應放在返回值類型列表的最後

對於返回值類型error,用來傳遞錯誤信息,在Golang中一般放在最後一個。



   
   
   
   

bool做爲返回值類型時也同樣。



   
   
   
   

姿式四:錯誤值統必定義,而不是跟着感受走

不少人寫代碼時,處處return errors.New(value),而錯誤value在表達同一個含義時也可能形式不一樣,好比「記錄不存在」的錯誤value可能爲:

  1. "record is not existed."
  2. "record is not exist!"
  3. "###record is not existed!!!"
  4. ...

這使得相同的錯誤value撒在一大片代碼裏,當上層函數要對特定錯誤value進行統一處理時,須要漫遊全部下層代碼,以保證錯誤value統一,不幸的是有時會有漏網之魚,並且這種方式嚴重阻礙了錯誤value的重構。

因而,咱們能夠參考C/C++的錯誤碼定義文件,在Golang的每一個包中增長一個錯誤對象定義文件,以下所示:



   
   
   
   

說明:筆者對於常量更喜歡C/C++的「全大寫+下劃線分割」的命名方式,讀者能夠根據團隊的命名規範或我的喜愛定製。

姿式五:錯誤逐層傳遞時,層層都加日誌

根據筆者經驗,層層都加日誌很是方便故障定位。

說明:至於經過測試來發現故障,而不是日誌,目前不少團隊還很難作到。若是你或你的團隊能作到,那麼請忽略這個姿式:)

姿式六:錯誤處理使用defer

咱們通常經過判斷error的值來處理錯誤,若是當前操做失敗,須要將本函數中已經create的資源destroy掉,示例代碼以下:



   
   
   
   

當Golang的代碼執行時,若是遇到defer的閉包調用,則壓入堆棧。當函數返回時,會按照後進先出的順序調用閉包。
對於閉包的參數是值傳遞,而對於外部變量倒是引用傳遞,因此閉包中的外部變量err的值就變成外部函數返回時最新的err值。
根據這個結論,咱們重構上面的示例代碼:



   
   
   
   

姿式七:當嘗試幾回能夠避免失敗時,不要當即返回錯誤

若是錯誤的發生是偶然性的,或由不可預知的問題致使。一個明智的選擇是從新嘗試失敗的操做,有時第二次或第三次嘗試時會成功。在重試時,咱們須要限制重試的時間間隔或重試的次數,防止無限制的重試。

兩個案例:

  1. 咱們平時上網時,嘗試請求某個URL,有時第一次沒有響應,當咱們再次刷新時,就有了驚喜。
  2. 團隊的一個QA曾經建議當Neutron的attach操做失敗時,最好嘗試三次,這在當時的環境下驗證果真是有效的。

姿式八:當上層函數不關心錯誤時,建議不返回error

對於一些資源清理相關的函數(destroy/delete/clear),若是子函數出錯,打印日誌便可,而無需將錯誤進一步反饋到上層函數,由於通常狀況下,上層函數是不關心執行結果的,或者即便關心也無能爲力,因而咱們建議將相關函數設計爲不返回error。

姿式九:當發生錯誤時,不忽略有用的返回值

一般,當函數返回non-nil的error時,其餘的返回值是未定義的(undefined),這些未定義的返回值應該被忽略。然而,有少部分函數在發生錯誤時,仍然會返回一些有用的返回值。好比,當讀取文件發生錯誤時,Read函數會返回能夠讀取的字節數以及錯誤信息。對於這種狀況,應該將讀取到的字符串和錯誤信息一塊兒打印出來。

說明:對函數的返回值要有清晰的說明,以便於其餘人使用。

異常處理的正確姿式

姿式一:在程序開發階段,堅持速錯

去年學習Erlang的時候,創建了速錯的理念,簡單來說就是「讓它掛」,只有掛了你纔會第一時間知道錯誤。在早期開發以及任何發佈階段以前,最簡單的同時也多是最好的方法是調用panic函數來中斷程序的執行以強制發生錯誤,使得該錯誤不會被忽略,於是可以被儘快修復。

姿式二:在程序部署後,應恢復異常避免程序終止

在Golang中,雖然有相似Erlang進程的Goroutine,但須要強調的是Erlang的掛,只是Erlang進程的異常退出,不會致使整個Erlang節點退出,因此它掛的影響層面比較低,而Goroutine若是panic了,而且沒有recover,那麼整個Golang進程(相似Erlang節點)就會異常退出。因此,一旦Golang程序部署後,在任何狀況下發生的異常都不該該致使程序異常退出,咱們在上層函數中加一個延遲執行的recover調用來達到這個目的,而且是否進行recover須要根據環境變量或配置文件來定,默認須要recover。
這個姿式相似於C語言中的斷言,但仍是有區別:通常在Release版本中,斷言被定義爲空而失效,但須要有if校驗存在進行異常保護,儘管契約式設計中不建議這樣作。在Golang中,recover徹底能夠終止異常展開過程,省時省力。

咱們在調用recover的延遲函數中以最合理的方式響應該異常:

  1. 打印堆棧的異常調用信息和關鍵的業務信息,以便這些問題保留可見;
  2. 將異常轉換爲錯誤,以便調用者讓程序恢復到健康狀態並繼續安全運行。

咱們看一個簡單的例子:



   
   
   
   

咱們指望test函數的輸出是:



   
   
   
   

實際上test函數的輸出是:



   
   
   
   

緣由是panic異常處理機制不會自動將錯誤信息傳遞給error,因此要在funcA函數中進行顯式的傳遞,代碼以下所示:



   
   
   
   

姿式三:對於不該該出現的分支,使用異常處理

當某些不該該發生的場景發生時,咱們就應該調用panic函數來觸發異常。好比,當程序到達了某條邏輯上不可能到達的路徑:



   
   
   
   

姿式四:針對入參不該該有問題的函數,使用panic設計

入參不該該有問題通常指的是硬編碼,咱們先看「一個啓示」一節中提到的兩個函數(Compile和MustCompile),其中MustCompile函數是對Compile函數的包裝:



   
   
   
   

因此,對於同時支持用戶輸入場景和硬編碼場景的狀況,通常支持硬編碼場景的函數是對支持用戶輸入場景函數的包裝。
對於只支持硬編碼單一場景的狀況,函數設計時直接使用panic,即返回值類型列表中不會有error,這使得函數的調用處理很是方便(沒有了乏味的"if err != nil {/ 打印 && 錯誤處理 /}"代碼塊)。

小結

本文以Golang爲例,闡述了錯誤和異常的區別,而且分享了不少錯誤和異常處理的正確姿式,這些姿式能夠單獨使用,也能夠組合使用,但願對你們有一點啓發。



func Compile(expr string) (*Regexp, error) func MustCompile(str string) *Regexpfunc (self *AgentContext) CheckHostType(host_type string) error { switch host_type { case "virtual_machine": return nil case "bare_metal": return nil } return errors.New("CheckHostType ERROR:" + host_type) }func (self *AgentContext) IsValidHostType(hostType string) bool { return hostType == "virtual_machine" || hostType == "bare_metal" }func (self *CniParam) setTenantId() error { self.TenantId = self.PodNs return nil }err := self.setTenantId() if err != nil { // log // free resource return errors.New(...) }func (self *CniParam) setTenantId() { self.TenantId = self.PodNs }self.setTenantId()resp, err := http.Get(url) if err != nil { return nill, err }value, ok := cache.Lookup(key) if !ok { // ...cache[key] does not exist… }var ERR_EOF = errors.New("EOF") var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe") var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error") var ERR_SHORT_BUFFER = errors.New("short buffer") var ERR_SHORT_WRITE = errors.New("short write") var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")func deferDemo() error { err := createResource1() if err != nil { return ERR_CREATE_RESOURCE1_FAILED } err = createResource2() if err != nil { destroyResource1() return ERR_CREATE_RESOURCE2_FAILED } err = createResource3() if err != nil { destroyResource1() destroyResource2() return ERR_CREATE_RESOURCE3_FAILED } err = createResource4() if err != nil { destroyResource1() destroyResource2() destroyResource3() return ERR_CREATE_RESOURCE4_FAILED } return nil }func deferDemo() error { err := createResource1() if err != nil { return ERR_CREATE_RESOURCE1_FAILED } defer func() { if err != nil { destroyResource1() } }() err = createResource2() if err != nil { return ERR_CREATE_RESOURCE2_FAILED } defer func() { if err != nil { destroyResource2() } }() err = createResource3() if err != nil { return ERR_CREATE_RESOURCE3_FAILED } defer func() { if err != nil { destroyResource3() } }() err = createResource4() if err != nil { return ERR_CREATE_RESOURCE4_FAILED } return nil }func funcA() error { defer func() { if p := recover(); p != nil { fmt.Printf("panic recover! p: %v", p) debug.PrintStack() } }() return funcB() } func funcB() error { // simulation panic("foo") return errors.New("success") } func test() { err := funcA() if err == nil { fmt.Printf("err is nil\\n") } else { fmt.Printf("err is %v\\n", err) } }err is fooerr is nilfunc funcA() (err error) { defer func() { if p := recover(); p != nil { fmt.Println("panic recover! p:", p) str, ok := p.(string) if ok { err = errors.New(str) } else { err = errors.New("panic") } debug.PrintStack() } }() return funcB() }switch s := suit(drawCard()); s { case "Spades": // ... case "Hearts": // ... case "Diamonds": // ... case "Clubs": // ... default: panic(fmt.Sprintf("invalid suit %v", s)) }func MustCompile(str string) *Regexp { regexp, error := Compile(str) if error != nil { panic(`regexp: Compile(` + quote(str) + `): ` + error.Error()) } return regexp }



相關文章
相關標籤/搜索