這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。Go 語言的錯誤處理是基於明確的目的而設計的。你應該從函數中返回所有可能的錯誤,並且檢查/處理這些傳回值。和其他語言相比,這一點可能看起來有些繁瑣和不人性化,其實並不是這樣的。讓我們來看看一些基本的例子,然後繼續做一些較重要的事情。## Non 錯誤實際上 Go 有個概念 non-error。這是一個語言特性,不能用在使用者自訂函數中。最明顯的例子就是從 map 中通過 key 擷取值。```goif val, ok := data["key"]; ok {// key/value 在 map 中存在}```當嘗試擷取指定 key 的值的時候,會返回一個可選的第二個值,是一個 boolean 類型表示擷取的值是否存在。```gofunc main() {a := map[string]string{"key": "value"}b := a["key"]c, ok := a["key"]d := a["foo"]fmt.Printf("%#v %#v %#v %#v %#v", a, b, c, d, ok)}```運行這段[程式](https://play.golang.org/p/CAZSyh9_q3)不會有任何錯誤。你能看到是否接受第二個傳回值是完全可選的。另一個例子是從 channel 中成功讀取資料。同樣的,你可以在讀取操作返回時,使用變數來接收第二個傳回值。```goif j, ok := <-jobs; ok {fmt.Println("received job", j)} else {fmt.Println("received all jobs")done <- true}```第二個參數是一個 boolean 類型,表示語言結構層面的成功或失敗,並不是一個嚴格的傳回型別。你可以寫一個函式宣告 `func() (interface{}, bool)` 同上面的代碼語義相同,但是不能夠忽略第二個參數 bool 傳回值了,你需要為他指定一個接收變數。## 忽略錯誤Go 提供了足夠的靈活性可以讓你忽略指定的返回錯誤。例如你想轉換一個字串到數字類型,並且你不在意轉換失敗時返回 0 。你可以使用 `_` 字元來忽略指定的傳回值,在下面例子中忽略了 error 傳回值: ```gov := "abc"s, _ := strconv.Atoi(v)fmt.Printf("%d\n", s)```很明顯轉換不會成功,在這處理 “invalid syntax” 錯誤會很繁瑣。當然這取決於你的使用情境,有一些情境處理返回錯誤沒什麼價值。我近期遇到的一個例子是 [sony/sonyflake](https://github.com/sony/sonyflake) 。 這個項目是一個 ID 產生器,返回 int64 類型的 id 和可能的錯誤。> 想要產生一個新的 id ,你只要調用 NextID 方法即可。> `func (sf *Sonyflake) NextID() (uint64, error)`> NextID 能夠連續產生 ID 從開始時間到 174 年左右。當超過這個限制的時候,NextID 會返回一個錯誤。我非常確信在看這篇文章的人不會活過174年。在這種情況下,你真的需要處理那個特定的錯誤嗎?這裡真的需要返回一個錯誤嗎?我認為這是一個設計缺陷,我們可以使用 Go 的另一個靈活性來更好地處理:`panic`。參見一篇很棒的文章 [go by example](https://gobyexample.com/panic):> 使用 panic 的一個通用的情境就是如果一個函數返回了一個錯誤值,但是我們不知道或者不希望去處理的時候中斷執行。還有一些其他的關於忽略返回錯誤的例子。可能最常見的忽略返回錯誤的處理方式是在 [json.Marshal](https://gobyexample.com/json) 中。在明確的理解之後,有些錯誤在第一次發生時可以不去處理。## 連續的錯誤處理你的目的應該是處理所有的錯誤,像下面這樣結束執行相對比較容易:```gobase64decoder := base64.NewDecoder(base64.StdEncoding, r.Body)gz, err := zlib.NewReader(base64decoder)if err != nil {return err}defer gz.Close()decoder := json.NewDecoder(gz)var t SentryV6Noticeerr = decoder.Decode(&t)if err != nil {return err}r.Body.Close()// ...```如果能像處理 if 語句一樣,獨立處理每個返回錯誤不是更好嗎?讓我們看一些你不知道的情況: `if func1() || func2() || func3() {`這個 if 語句會分別測試每個運算式。也就是說如果 `func1()` 返回了 false,那麼 `func2` 和 `func3` 函數就不會被調用。if 語句可以中斷執行流程,儘管如此也沒有方法使用一條陳述式完成檢查返回錯誤。 至少你可以按照下面的方法來處理: ```goif gz, err := zlib.NewReader(base64decoder); err != nil {return err}// ...if err := decoder.Decode(&t); err != nil {return err}```這個例子中,我們在運算式的前面添加了一個簡單的語句,這條語句會在測試運算式之前執行。這是 [Go 語言規範](https://golang.org/ref/spec#If_statements)的另一個特性。可惜的是,我們不能使用這個特性來控製程序流程。但是,我們可以考慮建立一個可變參函數來接收 `func() error` 參數,並且在第一個錯誤發生時立即返回。```gofunc flow(fns ...func() error) error {for _, fn := range fns {if err := fn(); err != nil {return err}}return nil}```這裡例子 [playground example](https://play.golang.org/p/AStZiZ_-Ml) 示範了如何?一個順序調用函數的處理,並且所做的修改不會影響到函數結構。基本上只依賴於如何儲存函數傳回值 除了返回錯誤之外。如果你正在處理 `jmoiron/sqlx` 你可以[這麼寫](https://play.golang.org/p/W-QEybSQwG):```goerr := flow(func() error { return db.Get(result, "select one row") },func() error { return db.Select(result, "select multiple rows") },func() error { return db.Get(result, "select another row") },)```一個明顯的問題就是過於冗餘的部分,把一個局部函數封裝到函數簽名中,如果程式設計語言的語義允許在 if 語句中(多次)賦值和測試的話,出錯檢查可以簡化成:```govar err errorif err = db.Get(result, "select one row") ||err = db.Select(result, "select multiple rows") ||err = db.Get(result, "select another row") {return err)```可惜的是,Go 不會把 errors 作為 boolean 運算式處理,也不允許在 boolean 運算式中賦值,會提示錯誤 ”expected boolean expression, found simple statement (missing parentheses around composite literal?)“ 。對於這一點我並不是很強烈的認為不好,因為還有其他的方法可以做到。如果你在其他語言例如 Node 或者 PHP,嘗試使用賦值給一個變數來代替測試一個值的話,你會發現 Go 的處理方式更加優美,注意:有時也非常痛苦。## 改進錯誤處理通常我寫的包括輸入,輸出參數的驗證函式,功能函數的函數簽名是 `func() error`。來看一個更複雜些的例子,我建立了一個 response writer 輸入參數為 interface{} 把第一個非空值或函數傳回值寫入 `http.ResponseWriter`。```go// JSON responds with the first non-nil payload, formats error messagesfunc JSON(w http.ResponseWriter, responses ...interface{}) {respond := func(payload interface{}) {json, err := json.Marshal(payload)if err != nil {http.Error(w, err.Error(), http.StatusInternalServerError)return}w.Header().Set("Content-Type", "application/json")w.Write(json)}for _, response := range responses {switch value := response.(type) {case nil:continuecase func() error:err := value()if err == nil {continue}respond(Error(err))case error:respond(Error(value))default:respond(struct {Response interface{} `json:"response"`}{response})}// Exit on the first output...break}}```這個函數的好處是提供了基於可變參數的條件執行處理。和傳入 `...error` 和 `[]error` 不同的是不需要先執行所有的函數。使用這個函數的一個簡單的 API 呼叫範例如下:```goinput := RequestInput{}result := RequestResult{}validate := func() error {// write and validate things for input}process := func() error {// write things to result, return error if any}resputil.JSON(w, validate, process, result)```在後面的語句 `resputil.JSON` 中可以看出,程式執行流程是顯而易見並且明確的,當有任何錯誤發生時都會中斷執行並返回。另一個附帶的好處是,無論何時發生錯誤了,你只要返回錯誤即可,不需要關心在這裡應該返回的其他傳回值,因為會在外部的閉包中被處理,並且只處理一次。> 注意:語言需要支援函數 `return err` 而不僅僅只是返回錯誤資訊,當你使用這種處理方法時,所有其他的傳回值都是未初始化時的預設值,所以不違反期待函數傳回值的規則,在這裡也有類似的建議 [this issue filed for Go2](https://github.com/golang/go/issues/21161#issuecomment-318350273)## 非同步錯誤處理幾周前在 [reddit](https://www.reddit.com/r/golang/comments/77bf8c/function_composition_in_go_with_reflect/dokx20g/) 上有過一個討論, @ligustah 推薦看一下 x/sync/errgroup 包,這個包提供了類似 sync.WaitGroup 實現方式的 Group structure ,會返回傳生的第一個錯誤或者在沒有錯誤時返回 nil 。每個函數都可以在 goroutines 中執行。 引用至 godoc 中的例子 [JustErrors](https://godoc.org/golang.org/x/sync/errgroup#ex-Group--JustErrors) ```govar g errgroup.Groupvar urls = []string{"http://www.golang.org/","http://www.google.com/","http://www.somestupidname.com/",}for _, url := range urls {// Launch a goroutine to fetch the URL.url := url // https://golang.org/doc/faq#closures_and_goroutinesg.Go(func() error {// Fetch the URL.resp, err := http.Get(url)if err == nil {resp.Body.Close()}return err})}// Wait for all HTTP fetches to complete.if err := g.Wait(); err == nil {fmt.Println("Successfully fetched all URLs.")}```開始的時候,我覺得這個包還不錯,但是裡面有一些需要注意的地方。1. 你可能需要返回所有的錯誤(你只能拿到第一個) 2. 在發生錯誤的時候想要中斷執行(必須等到所有的 goroutines 執行完畢) 3. 執行的是並行檢查(沒有提供順序執行的 API )根據你的使用情境,可以參照這個模型,做一些私人的實現。這個包本身有些繁瑣,看了前面的介紹,寫一個聰明的錯誤檢查封裝函數只需要少數的幾行代碼即可。## 最後總結有些時候看起來 Go 語言檢查傳回值錯誤是比較痛苦的事情,特別是當你受到有 try/catch 特性的語言影響的時候,例如: Java,PHP 等。當你第一次看到 Go 的這種處理方式的時候可能並不喜歡,希望檢查錯誤的處理能更好些(更簡潔些?),我相信其他的語言有更糟糕的例子,更加繁瑣,更多不足的地方。 errors 和 panics 有一些不同的地方,實際上 panics 是帶有函數調用棧資訊的。如果你想在 errors 裡面添加調用棧資訊,我推薦你使用 Dave Cheney 的 [pkg/errors](https://dave.cheney.net/2016/06/12/stack-traces-and-the-errors-package) 包。文章 [Don’t just check errors, handle them gracefully](https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully) 對每個人來說都是在必讀列表中的。在其他語言中對錯誤的處理有一些顯著的痛點,如果 Go 的處理有一點繁瑣的話,那麼它帶來的是更多的好處。
via: https://scene-si.org/2017/11/13/error-handling-in-go/
作者:Tit Petric 譯者:tyler2018 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
798 次點擊 ∙ 1 贊