優雅的處理錯誤,而不僅僅只是檢查錯誤

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。這篇文章摘取至我在日本東京舉辦的 [GoCon spring conference](https://gocon.connpass.com/event/27521/) 上的演講稿。![](https://raw.githubusercontent.com/studygolang/gctt-images/master/error-handle/ba5a9ada.png)## 錯誤只是一些值我花了很多時間來思考如何在 Go 中處理錯誤是最好的。我真希望能有一種簡單直接的方式來處理錯誤,一些我們只要讓 Go 程式員記住就能使用的規則,就像教數學或字母表一樣。然而,我得到的結論是:處理錯誤不止有一種方式。我認為 Go 處理錯誤的方式可以劃分為 3 種主要的策略。## 標記錯誤策略第一種錯誤處理策略,我稱之為標記錯誤 ```goif err == ErrSomething { … }```這個名字來源於在實際編程中,使用一個指定的值來表示程式已經無法繼續執行。所以在 Go 中我們使用一個指定的值來表示錯誤。例如:系統包裡面的 `io.EOF` 或是在 `syscall` 包中更底層些的常量錯誤例如 `syscall.ENOENT`。甚至還有標記表示沒有錯誤發生例如:`path/filepath.Walk` 中的 `go/build.NoGoError` 和 `path/filepath.SkipDir`。使用標記值是靈活性最差的一種錯誤處理策略,調用者必須使用相等操作符來比較傳回值和預先定義的值。當你想要提供更多的相關資訊時,返回不同的錯誤值會破壞等式檢查操作。 即使通過 `fmt.Errorf` 來提供更多的資訊也會干擾調用者的等式測試,調用者必須去看 Error 方法輸出的結果是否匹配某個指定的字串。### 永遠不要檢查 `error.Error` 的輸出順便說一下,我相信你永遠都不需要檢查 `error.Error` 方法的傳回值。 `error` 介面中的 `Error` 方法是提供給使用者查看的資訊,而不是用來給代碼做判斷的。這些資訊應該在記錄檔中或者是顯示屏上出現,你不需要通過檢查這些資訊來改變程式行為。我知道有時候這樣很難,就像有些人在 twitter 上提到的那樣,這條建議在寫測試的時候不適用。儘管如此,在我看來,作為一種編碼風格,你應該避免比較字元型的錯誤資訊。### 標記錯誤成為公開 API 的一部分如果你的公開函數或方法返回了一些指定的錯誤值,那麼這些值必須是公開的,當然也需要在文檔中有所描述。這些加入到你的 API 中了。如果你的 API 定義了一個返回指定錯誤的介面,那麼所有該介面的實現都必須只返回這個錯誤,就算能提供更多的其他資訊也不應該返回除了指定錯誤之外的資訊。我們可以在 `io.Reader` 中看到這樣的處理方式。 `io.Copy` 要求 reader 實現返回 `io.EOF` 通知調用者沒有更多的資料了,但是這並不是一個錯誤。### 標記錯誤在兩個包之間製造了依賴關係最大的問題是標記錯誤在兩個包之間製造了源碼層面的依賴關係。例如:檢查一個錯誤是否是 `io.EOF` 你的代碼必須引入 `io` 包。這個例子看起來沒那麼糟糕,因為這是很普通的操作。但是想象一下,項目中很多包匯出錯誤值,而其他包必須匯入對應的包才能檢查錯誤條件,這樣就違背了低耦合的設計原則。我參與過的一個大型項目,使用的就是這種錯誤處理模式,我可以告訴你不好的設計所帶來的迴圈引入問題近在咫尺。### 結論:避免使用標記錯誤策略所以,我的建議是避免在代碼中使用標記錯誤處理策略。在標準庫中有些情況使用了這種處理方式,但是這並不是你應該效仿的一種處理模式。如果有人要求你從你的包裡面暴露一個錯誤值,你應該禮貌的拒絕他,並提供一個替代方案,也就是下面將要提到的方法。## 錯誤類型錯誤類型是我想討論的第二種 Go 錯誤處理模式。```goif err, ok := err.(SomeType); ok { … }```錯誤類型是你建立的實現 error 介面的類型。在下面的例子中, MyError 類型記錄了檔案,行號,相關的錯誤資訊。```gotype MyError struct {Msg stringFile stringLine int}func (e *MyError) Error() string {return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Msg)}return &MyError{"Something happened", "server.go", 42}```因為 MyError 是一個類型,所以調用者可以使用 type assertion 從 error 中擷取相關資訊。```goerr := something()switch err := err.(type) {case nil:// call succeeded, nothing to docase *MyError:fmt.Println(“error occurred on line:”, err.Line)default:// unknown error}```錯誤類型比標記錯誤最大的改進就是通過封裝底層的錯誤來提供更多的相關資訊。一個絕佳的例子就是 os.PathError 除了底層錯誤外還提供了使用哪個檔案,執行哪個操作等相關資訊。```go// PathError records an error and the operation// and file path that caused it.type PathError struct {Op stringPath stringErr error // the cause}func (e *PathError) Error() string```### 錯誤類型存在的問題調用者可以使用 type assertion 或者 type switch,error 類型必須是公開的。 如果你的代碼實現了一個約定指定錯誤類型的介面,那麼所有這個介面的實現者,都要依賴於定義這個錯誤類型的包。 對包的錯誤類型的過度暴露,使調用者和包之間產生了很強的耦合性,導致了 API 的脆弱性。### 結論:避免錯誤類型雖然錯誤類型在發生錯誤時能夠捕捉到更多的環境資訊,比標記錯誤要好一些,但是錯誤類型也存在很多和標記錯誤一樣的問題。所以,在這裡我的建議是避免使用錯誤類型,至少避免使他們成為你 API 介面的一部分。## 封裝錯誤現在我們到了第三個錯誤處理分類。在我看來這個是靈活性最好的處理策略,在調用者和你的代碼之間產生的耦合度最低。我管這種處理方式叫做封裝錯誤 (Opaque errors),因為當你發現有錯誤發生時,你無法知道內部的錯誤情況。作為調用者,你只知道調用的結果成功或者失敗。封裝錯誤處理方式只返回錯誤不去猜測他的內容。如果你採用了這種處理方式,那麼錯誤處理在調試方面會變得非常有價值。```goimport “github.com/quux/bar”func fn() error {x, err := bar.Foo()if err != nil {return err}// use x}```例如:Foo 的呼叫慣例沒有指定在發生錯誤時會返回哪些相關資訊,這樣 Foo 函數的開發人員就可以自由的提供相關錯誤資訊,並且不會影響到和調用者之間的約定。### 斷言行為,而不是類型在少數情況下,這種二元錯誤處理方案是不夠的。例如:在和進程外互動的時候,比如網路活動,需要調用者評估錯誤情況來決定是否需要重試操作。在這種情況下我們斷言錯誤實現指定的行為要比斷言指定類型或值好些。看看下面的例子:```gotype temporary interface {Temporary() bool}// IsTemporary returns true if err is temporary.func IsTemporary(err error) bool {te, ok := err.(temporary)return ok && te.Temporary()}```我們可以傳任何錯誤給 IsTemporary 來判斷錯誤是否需要重試。如果錯誤沒有實現 temporary 介面;那麼就沒有 Temporary 方法,那麼錯誤就不是 temporary。如果錯誤實現了 Temporary,如果 Temporary 返回 true 那麼調用者就可以考慮重試該操作。這裡的關鍵點是這個實現邏輯不需要匯入定義錯誤的包或者瞭解任何關於錯誤的底層類型,我們只要簡單的關注它的行為即可。### 優雅的處理錯誤,而不僅僅只是檢查錯誤這引出了我想談的第二個 Go 語言的格言:優雅的處理錯誤,而不僅僅只是檢查錯誤。你能在下面的代碼中找出錯誤嗎?```gofunc AuthenticateRequest(r *Request) error {err := authenticate(r.User)if err != nil {return err}return nil}```一個很明顯的建議是上面的代碼可以簡化為```goreturn authenticate(r.User)```但是這隻是個簡單的問題,任何人在代碼審查的時候都應該看到。更根本的問題是這段代碼看不出來原始錯誤是在哪裡發生的。如果 authenticate 返回錯誤, 那麼 AuthenticateRequest 將會返回錯誤給調用者,調用者也一樣返回。 在程式的最上一層主函數塊內列印錯誤資訊到螢幕或者記錄檔,然而所有資訊就是 No such file or directory![](https://raw.githubusercontent.com/studygolang/gctt-images/master/error-handle/53c71467.png)沒有錯誤發生的檔案,行號等資訊,也沒有調用棧資訊。代碼的編寫者必須在一堆函數中尋找哪個調用路徑會返回 file not found 錯誤。Donovan 和 Kernighan 寫的 The Go Programming Language 建議你使用 fmt.Errorf 在錯誤路徑中增加相關資訊```gofunc AuthenticateRequest(r *Request) error {err := authenticate(r.User)if err != nil {return fmt.Errorf("authenticate failed: %v", err)}return nil}```就像我們在前面提到的,這個模式不相容標記錯誤或者類型斷言,因為轉換錯誤值到字串,再和其他的字串合并,再使用 fmt.Errorf 轉換為error 打破了對等關係,破壞了原始錯誤的相關資訊。### 註解錯誤我在這裡建議一個給錯誤添加相關資訊的方法,要用到一個簡單的包。代碼在 [github.com/pkg/errors](https://godoc.org/github.com/pkg/errors)。這個包有兩個主要的函數: ```go// Wrap annotates cause with a message.func Wrap(cause error, message string) error```第一個函數是封裝函數 Wrap ,輸入一個錯誤和一個資訊,產生一個新的錯誤返回。```go// Cause unwraps an annotated error.func Cause(err error) error```第二個函數是 Cause ,輸入一個封裝過的錯誤,解包之後得到原始的錯誤資訊。使用這兩個函數,我們現在可以給任何錯誤添加相關資訊,並且在我們需要查看底層錯誤類型的時候可以解包查看。下面的例子是讀取檔案內容到記憶體的函數。```gofunc ReadFile(path string) ([]byte, error) {f, err := os.Open(path)if err != nil {return nil, errors.Wrap(err, "open failed")}defer f.Close()buf, err := ioutil.ReadAll(f)if err != nil {return nil, errors.Wrap(err, "read failed")}return buf, nil}```我們使用這個函數寫一個讀取設定檔的函數,然後在 main 中調用。```gofunc ReadConfig() ([]byte, error) {home := os.Getenv("HOME")config, err := ReadFile(filepath.Join(home, ".settings.xml"))return config, errors.Wrap(err, "could not read config")}func main() {_, err := ReadConfig()if err != nil {fmt.Println(err)os.Exit(1)}}```如果 ReadConfig 發生錯誤,由於使用了 errors.Wrap,我們可以得到一個 K&D 風格的包含相關資訊的錯誤```could not read config: open failed: open /Users/dfc/.settings.xml: no such file or directory```因為 errors.Wrap 產生了發生錯誤時的調用棧資訊,所以我們可以查看額外的調用棧調試資訊。這又是一個同樣的例子,但是這次我們用 errors.Print 替換 fmt.Println```gofunc main() {_, err := ReadConfig()if err != nil {errors.Print(err)os.Exit(1)}}```我們會得到如下的資訊:```readfile.go:27: could not read configreadfile.go:14: open failedopen /Users/dfc/.settings.xml: no such file or directory```第一行來至 ReadConfig, 第二行來至 os.Open 的 ReadFile, 剩下的來至 os 包,沒有攜帶位置資訊。現在我們介紹了關於打包錯誤產生棧的概念,我們需要談談如何解包。下面是 errors.Cause 函數的作用。```go// IsTemporary returns true if err is temporary.func IsTemporary(err error) bool {te, ok := errors.Cause(err).(temporary)return ok && te.Temporary()}```操作中,當你需要檢查一個錯誤是否匹配一個指定值或類型時,你需要先使用 errors.Cause 擷取原始錯誤資訊### 只處理一次錯誤最後我想要說的是,你只需要處理一次錯誤。處理錯誤意味著檢查錯誤值,然後作出決定。```gofunc Write(w io.Writer, buf []byte) {w.Write(buf)}```如果你不需要做出決定,你可以忽略這個錯誤。在上面的例子可以看到我們忽略了 `w.Write` 返回的錯誤。但是在返回一個錯誤時做出多個決定也是有問題的。```gofunc Write(w io.Writer, buf []byte) error {_, err := w.Write(buf)if err != nil {// annotated error goes to log filelog.Println("unable to write:", err)// unannotated error returned to callerreturn err}return nil}```在這個例子中,如果 Write 發生錯誤, 一行資訊會寫入日誌,記錄發送錯誤的檔案和行號,同時把錯誤返回給調用者,同樣的調用者也可能會寫入日誌,然後返回,直到程式的最頂層。記錄檔裡就會出現一堆重複的資訊,但是在程式最頂層獲得的原始錯誤卻沒有任何相關資訊。```gofunc Write(w io.Write, buf []byte) error {_, err := w.Write(buf)return errors.Wrap(err, "write failed")}```使用 errors 包可以讓你在 error 裡面加入相關資訊,並且內容是可以被人和機器所識別的。## 結論最後,錯誤是你提供的包中公開 API 的一部分,要像其他公開 API 一樣小心對待。為了獲得最大的靈活性,我建議你嘗試把所有的錯誤當做封裝錯誤來處理,在那些無法做到的情況下,斷言行為錯誤,而不是類型或值。儘可能少的在你程式中使用標記錯誤,在錯誤發生時儘早的使用 errors.Wrap 打包封裝為封裝錯誤。## 相關文章1. [檢查錯誤](https://dave.cheney.net/2014/12/24/inspecting-errors)2. [常量錯誤](https://dave.cheney.net/2016/04/07/constant-errors)3. [調用棧和錯誤包](https://dave.cheney.net/2016/06/12/stack-traces-and-the-errors-package)4. [返回的錯誤和異常](https://dave.cheney.net/2016/06/12/stack-traces-and-the-errors-package)

via: https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully

作者:Dave Cheney 譯者:tyler2018 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

606 次點擊  
相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.