Golang Error Handling 是好的設計嗎?

從Java、C++、PHP轉過來的Gopher在遇到錯誤處理時都會很苦惱,與前者們的相似try/catch模式相比,Golang的檢查返回值判斷錯誤的寫法顯得特別繁瑣。本文試圖去探究下Golang中Error Handling設計的背景與思惟過程,力求還原一個真實的設計權衡。

一、標準包中的Error Handling

Golang標準包提供的Error Handling功能是經過error這個interface實現的。git

type error interface {
    Error() string
}

因此自定義一個error類型很簡單,只要實現Error方法便可:github

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

那麼如何處理錯誤呢?
官方推薦使用相似於C語言的,檢查返回值的方式(if err != nil),例如:golang

func readfile(path string) error {
    err := openfile(path)
    if err != nil {
        return fmt.Errorf("cannot open file: %v", err)
    }
    //...
}

以上可能還看不出問題,好比下面這樣:spring

func DoSomething() (*C, error) {
   var err error
   var a string
   a, err =DoA()
   if err == nil {
      var b string
      b, err = DoB(a)
      if err == nil {
         var c string
         c, err = DoC(b)
         if err == nil {
            return c, nil
         }
      }
   }
   return nil, err
}

每次調用一個函數都須要作一次錯誤檢查,這使得代碼寫起來十分繁瑣,而不像其餘編程語言中的try/catch來的簡潔:錯誤統一處理,開發者專一當前業務流程。編程

二、爲何是這個樣子?

Golang的錯誤處理模式受到了許多批評的聲音,因而Rob Pike不得不在官網刊文解釋,並聲明『Errors are values』:Values can be programmed, and since errors are values, errors can be programmed.,即開發者能夠自定義error類型,優雅地設計符合自身業務流程的錯誤處理方式。json

但同時他再次強調到:無論怎麼設計,在程序中檢查暴露出來的錯誤都是相當重要的。
一直到文章最後,Rob Pike都沒有解釋爲何要這麼設計,而是在闡述如何優雅地使用Error Handling。網絡

繼續搜索之,官方的FAQ道出了設計背後考慮:We believe that coupling exceptions to a control structure, as in the try-catch-finally idiom, results in convoluted code. 即官方認爲將異常耦合到控制結構(就像嘗試try-catch-finally習慣用法同樣)會致使複雜的代碼,並鼓勵開發者去顯示地檢查錯誤,這樣程序流程不會被打斷。這也是爲何Golang會提供多值返回的緣由之一。雖然與其餘編程語言不一樣, 但規範化的錯誤類型,加上Golang的其餘特性(如error可編程),使錯誤處理變得方便。編程語言

總結起來,Error Handling的設計初衷是拿編碼的冗餘性換取了邏輯的簡單性,這即是其中的tradeoff,正如 Go proverbs所言:Clear is better than clever.ide

三、pkg包中的Error Handling

使用錯誤處理的姿式每每不止一種,標準庫裏經過的Error接口只夠知足通常簡單的場景,對於某些須要瞭解錯誤細節(如判斷錯誤是網絡中斷仍是返回值格式問題)以及當前執行棧的場景,則須要對Error接口的進一步封裝,目前比較推崇的是github.com/pkg/errors包。函數

它除了輸出調用層級上每一級錯誤內容以外,還能夠打印出發生錯誤的文件名與所在行號,這對於定位Bug很是有幫助。
使用方法也很簡單,能夠看下面一個例子:

package main

import (
    "fmt"

    "github.com/pkg/errors"
)

func A() error {
    return errors.New("NullPointerException")
}

func B() error {
    return A()
}

func main() {
    fmt.Printf("Error: %+v", B())
}

運行輸出:
圖片描述

四、Golang 2 草案

彷佛開發者們已經習慣了try-catch-finally的用法,對於官方給出的Error Handling的設計解釋並不買帳,因而在Golang 2 草案中,Error Handling做爲一個重大改變被提了出來,並給出了優化方案:增長checkhandle兩個新關鍵字。
這裏有兩個對比示例:

Go 1

type Parsed struct { ... }

func ParseJson(name string) (Parsed, error) {

    // Open the file
    f, err := os.Open(name)
    if err != nil {
        return fmt.Errorf("parsing json: %s %v", name, err)
    }
    defer f.Close()

    // Parse json into p
    var p Parsed
    err = json.NewDecoder(f).Decode(&p)
    if err != nil {
        return fmt.Errorf("parsing json: %s %v", name, err)
    }

    return p
}

Go 2

type Parsed struct { ... }

func ParseJson(name string) (Parsed, error) {
    handle err {
        return fmt.Errorf("parsing json: %s %v", name, err)
    }

    // Open the file
    f := check os.Open(name)
    defer f.Close()

    // Parse json into p
    var p Parsed
    check json.NewDecoder(f).Decode(&p)

    return p
}

對比能夠發現:增長了check與hanle關鍵字以後,總體的代碼邏輯變得更加簡潔,錯誤能夠統一在handle處獲得處理,相似於try/catch.

  • check:負責顯示地檢查錯誤(標記)
  • handle:定義錯誤處理邏輯,一旦check到指定錯誤,便會進入相應的錯誤處理邏輯。

固然,做爲2.0的草案,並不必定會最終歸入Golang2的正式標準中,但如此好的設計值得期待。

五、References

https://blog.golang.org/error...
https://dave.cheney.net/paste...
https://blog.golang.org/error...
https://golang.org/doc/faq#ex...
https://medium.com/@hussachai...
https://blog.golang.org/go2draft
https://go.googlesource.com/p...
https://dev.to/deanveloper/go...

相關文章
相關標籤/搜索