Go語言錯誤處理

jopen 9年前發布 | 20K 次閱讀 Go語言 Google Go/Golang開發

近期閑暇用Go寫一個lib,其中涉及到error處理的地方讓我琢磨了許久。關于Go錯誤處理的資料和視頻已有許多,Go authors們也在官方Articles和Blog上多次提到過一些Go error handling方面的一些tips和best practice,這里僅僅算是做個收集和小結,盡視野所及,如有不足,歡迎評論中補充。(10月因各種原因,沒有耕博,月末來一發,希望未為晚矣 ^_^)

一、概述

Go是一門simple language,常拿出來鼓吹的就是作為gopher習以為傲的僅僅25個關鍵字^_^。因此Go的錯誤處理也一如既往的簡單。我們知道C語言錯誤處理以返 回錯誤碼(errno)為主流,目前企業第一語言Java則用try-catch- finally的處理方式來統一應對錯誤和異常(開發人員常常因分不清楚到底哪些是錯誤,哪些是異常而濫用該機制)。Go則繼承了C,以返回值為錯誤處理的主要方式(輔以panic與recover應對runtime異常)。但與C不同的是,在Go的慣用法中,返回值不是整型等常用返回值類型,而是用了一個 error(interface類型)。

type interface error {
    Error() string
}

這也體現了Go哲學中的“正交”理念:error context與error類型的分離。無論error context是int、float還是string或是其他,統統用error作為返回值類型即可。

func yourFunction(parametersList) (..., error)
func (Receiver)yourMethod(parametersList) (..., error)

在Andrew Gerrand的“Error handling and Go“一文中,這位Go authors之一明確了error context是由error接口實現者supply的。在Go標準庫中,Go提供了兩種創建一個實現了error interface的類型的變量實例的方法:errors.New和fmt.Errorf:

errors.New("your first error code")
fmt.Errorf("error value is %d\n", errcode)

這兩個方法實際上返回的是同一個實現了error interface的類型實例,這個unexported類型就是errorString。顧名思義,這個error type僅提供了一個string的context!

//$GOROOT/srcerrors/errors.go

type errorString struct {
    s string
}

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

這兩個方法也基本滿足了大部分日常學習和開發中代碼中的錯誤處理需求。

二、慣用法(idiomatic usage)

1、基本用法

就像上面函數或方法定義那樣:

func yourFunction(parametersList) (..., error)
func (Receiver)yourMethod(parametersList) (..., error)

通常情況,我們將函數或方法定義中的最后一個返回值類型定義為error。使用該函數或方法時,通過如下方式判斷錯誤碼:

..., err := yourFunction(...)
if err != nil {
    //error handling
}

or

if ..., err := yourFunction(...); err != nil {
    //error handling
}

2、注意事項

1)、永遠不要忽略(ignore)函數或方法返回的錯誤碼,Check it。(例外:包括標準庫在內的Go代碼很少去判斷fmt.Println or Printf系列函數的返回值)

2)、error的string context中的內容格式:頭母小寫,結尾不帶標點。因為考慮到error被經常這么用:

... err := errors.New("error example")
fmt.Printf("The returned error is %s.\n", err)

3)、error處理流的縮進樣式

prefer

..., err := yourFunction(...)
if err != nil {
    // handle error
}

//go on doing something.

rather than:

..., err := yourFunction(...)
if err == nil {
    // do something.
}

// handle error

三、槽點與破解之法

Go自誕生那天起就伴隨著巨大爭議,這也不奇怪,就像娛樂圈,如果沒有爭議,哪有存在感,刷臉的機會都沒有。看來有爭議是件好事,沒爭議的編程語言都已經成為了歷史。炒作懂么!這也是很多Gopher的微博、微信、推ter、medium賬號喜歡發“Why I do not like Go”類文章的原因吧^_^。

Go的error處理方式就是被詬病的點之一,反方主要論點就是Go的錯誤處理機制似乎回到了70年代(與C同齡^_^),使得錯誤處理代碼冗長且重復(部分也是由于前面提到的:不要ignore任何一個錯誤碼),比如一些常見的錯誤處理代碼形式如下:

err := doStuff1()
if err != nil {
    //handle error...
}

err = doStuff2()
if err != nil {
    //handle error...
}

err = doStuff3()
if err != nil {
    //handle error...
}

這里不想去反駁這些論點,Go authors之一的Russ Cox對于這種觀點進行過駁斥:當初選擇返回值這種錯誤處理機制而不是try-catch這種機制,主要是考慮前者適用于大型軟件,后者更適合小程序。當程序變大,try-catch會讓錯誤處理更加冗長繁瑣易出錯(具體參見go faq)。不過Russ Cox也承認Go的錯誤處理機制對于開發人員的確有一定的心智負擔。

好了,關于這個槽點的敘述點到為止,我們關心的是“如何破解”!Go的錯誤處理的確冗長,但使用一些tips,還是可以將代碼縮減至可以忍受的范圍的,這里列舉三種:

1、checkError style

對于一些在error handle時可以選擇goroutine exit(注意:如果僅存main goroutine一個goroutine,調用runtime.Goexit會導致program以crash形式退出)或os.Exit的情形,我們可以選擇類似常見的checkError方式簡化錯誤處理,例如:

func checkError(err error) {
    if err != nil {
        fmt.Println("Error is ", err)
        os.Exit(-1)
    }
}

func foo() {
    err := doStuff1()
    checkError(err)

    err = doStuff2()
    checkError(err)

    err = doStuff3()
    checkError(err)
}

這種方式有些類似于C中用宏(macro)簡化錯誤處理過程代碼,只是由于Go不支持宏,使得這種方式的應用范圍有限。

2、聚合error handle functions

有些時候,我們會遇到這樣的情況:

err := doStuff1()
if err != nil {
    //handle A
    //handle B
    ... ...
}

err = doStuff2()
if err != nil {
    //handle A
    //handle B
    ... ...
}

err = doStuff3()
if err != nil {
    //handle A
    //handle B
    ... ...
}

在每個錯誤處理過程,處理過程相似,都是handle A、handle B等,我們可以通過Go提供的defer + 閉包的方式,將handle A、handle B…聚合到一個defer匿名helper function中去:

func handleA() {
    fmt.Println("handle A")
}
func handleB() {
    fmt.Println("handle B")
}

func foo() {
    var err error
    defer func() {
        if err != nil {
            handleA()
            handleB()
        }
    }()

    err = doStuff1()
    if err != nil {
        return
    }

    err = doStuff2()
    if err != nil {
        return
    }

    err = doStuff3()
    if err != nil {
        return
    }
}

3、 將doStuff和error處理綁定

在Rob Pike的”Errors are values”一文中,Rob Pike told us 標準庫中使用了一種簡化錯誤處理代碼的trick,bufio的Writer就使用了這個trick:

b := bufio.NewWriter(fd)
    b.Write(p0[a:b])
    b.Write(p1[c:d])
    b.Write(p2[e:f])
    // and so on
    if b.Flush() != nil {
            return b.Flush()
        }
    }

我們看到代碼中并沒有判斷三個b.Write的返回錯誤值,錯誤處理放在哪里了呢?我們打開一下$GOROOT/src/

type Writer struct {
    err error
    buf []byte
    n   int
    wr  io.Writer
}

func (b *Writer) Write(p []byte) (nn int, err error) {
    for len(p) > b.Available() && b.err == nil {
        ... ...
    }
    if b.err != nil {
        return nn, b.err
    }
    ......
    return nn, nil
}

我們可以看到,錯誤處理被綁定在Writer.Write的內部了,Writer定義中有一個err作為一個錯誤狀態值,與Writer的實例綁定在了一起,并且在每次Write入口判斷是否為!= nil。一旦!=nil,Write其實什么都沒做就return了。

以上三種破解之法,各有各的適用場景,同樣你也可以看出各有各的不足,沒有普適之法。優化go錯誤處理之法也不會局限在上述三種情況,肯定會有更多的solution,比如代碼生成,比如其他還待發掘。

四、解調用者之惑

前面舉的例子對于調用者來講都是較為簡單的情況了。但實際編碼中,調用者不僅要面對的是:

if err != nil {
    //handle error
}

還要面對:

if err 是 ErrXXX
    //handle errorXXX

if err 是 ErrYYY
    //handle errorYYY

if err 是ErrZZZ
    //handle errorZZZ

我們分三種情況來說明調用者該如何處理不同類型的error實現:

1、由errors.New或fmt.Errorf返回的錯誤變量

如果你調用的函數或方法返回的錯誤變量是調用errors.New或fmt.Errorf而創建的,由于errorString類型是unexported的,因此我們無法通過“相當判定”或type assertion、type switch來區分不同錯誤變量的值或類型,唯一的方法就是判斷err.String()是否與某個錯誤context string相等,示意代碼如下:

func openFile(name string) error {
    if file not exist {
        return errors.New("file does not exist")
    }

    if have no priviledge {
        return errors.New("no priviledge")
    }
    return nil
}

func main() {
    err := openFile("example.go")
    if err.Error() == "file does not exist" {
        // handle "file does not exist" error
        return
    }

    if err.Error() == "no priviledge" {
        // handle "no priviledge" error
        return
    }
}

但這種情況太low了,不建議這么做!一旦遇到類似情況,就要考慮通過下面方法對上述情況進行重構。

2、exported Error變量

打開$GOROOT/src/os/error.go,你會在文件開始處發現如下代碼:

var (
    ErrInvalid    = errors.New("invalid argument")
    ErrPermission = errors.New("permission denied")
    ErrExist      = errors.New("file already exists")
    ErrNotExist   = errors.New("file does not exist")
)

這些就是os包export的錯誤碼變量,由于是exported的,我們在調用os包函數返回后判斷錯誤碼時可以直接使用等于判定,比如:

err := os.XXX
if err == os.ErrInvalid {
    //handle invalid
}
... ...

也可以使用switch case:

switch err := os.XXX {
    case ErrInvalid:
        //handle invalid
    case ErrPermission:
        //handle no permission
    ... ...
}
... ...

(至于error類型變量與os.ErrInvalid的可比較性可參考go specs

一般對于庫的設計和實現者而言,在庫的設計時就要考慮好export出哪些錯誤變量。

3、定義自己的error接口實現類型

如果要提供額外的error context,我們可以定義自己的實現error接口的類型;如果這些類型還是exported的,我們就可以用type assertion or type switch來判斷返回的錯誤碼類型并予以對應處理。

比如$GOROOT/src/net/net.go:

type OpError struct {
    Op string
    Net string
    Source Addr
    Addr Addr
    Err error
}

func (e *OpError) Error() string {
    if e == nil {
        return "<nil>"
    }
    s := e.Op
    if e.Net != "" {
        s += " " + e.Net
    }
    if e.Source != nil {
        s += " " + e.Source.String()
    }
    if e.Addr != nil {
        if e.Source != nil {
            s += "->"
        } else {
            s += " "
        }
        s += e.Addr.String()
    }
    s += ": " + e.Err.Error()
    return s
}

net.OpError提供了豐富的error Context,不僅如此,它還實現了除Error以外的其他method,比如:Timeout(實現net.timeout interface) 和Temporary(實現net.temporary interface)。這樣我們在處理error時,可通過type assertion或type switch將error轉換為*net.OpError,并調用到Timeout或Temporary方法來實現一些特殊的判定。

err := net.XXX
if oe, ok := err.(*OpError); ok {
    if oe.Timeout() {
        //handle timeout...
    }
}

五、坑(s)

每種編程語言都有自己的專屬坑(s),Go雖出身名門,但畢竟年輕,坑也不少,在error處理這塊也可以列出幾個。

1、 Go FAQ:Why is my nil error value not equal to nil?

type MyError string

func (e *MyError) Error() string {
    return string(*e)
}

var ErrBad = MyError("ErrBad")

func bad() bool {
    return false
}

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = &ErrBad
    }
    return p // Will always return a non-nil error.
}

func main() {
    err := returnsError()
    if err != nil {
        fmt.Println("return non-nil error")
        return
    }
    fmt.Println("return nil")
}

上面的輸出結果是”return non-nil error”,也就是說returnsError返回后,err != nil。err是一個interface類型變量,其underlying有兩部分組成:類型和值。只有這兩部分都為nil時,err才為nil。但returnsError返回時將一個值為nil,但類型為*MyError的變量賦值為err,這樣err就不為nil。解決方法:

func returnsError() error {
    var p *MyError = nil
    if bad() {
        p = &ErrBad
    }
    return nil
}

2、switch err.(type)的匹配次序

試想一下下面代碼的輸出結果:

type MyError string

func (e MyError) Error() string {
    return string(e)
}

func Foo() error {
    return MyError("foo error")
}

func main() {
    err := Foo()
    switch e := err.(type) {
    default:
        fmt.Println("default")
    case error:
        fmt.Println("found an error:", e)
    case MyError:
        fmt.Println("found MyError:", e)
    }
    return

}

你可能會以為會輸出:”found MyError: foo error”,但實際輸出卻是:”found an error: foo error”,也就是說e先匹配到了error!如果我們調換一下次序呢:

... ...
func main() {
    err := Foo()
    switch e := err.(type) {
    default:
        fmt.Println("default")
    case MyError:
        fmt.Println("found MyError:", e)
    case error:
        fmt.Println("found an error:", e)
    }
    return
}

這回輸出結果變成了:“found MyError: foo error”。

也許你會認為這不全是錯誤處理的坑,和switch case的匹配順序有關,但不可否認的是有些人會這么去寫代碼,一旦這么寫,坑就踩到了。因此對于通過switch case來判定error type的情況,將error這個“通用”類型放在后面或去掉。

六、第三方庫

如果覺得go內置的錯誤機制不能很好的滿足你的需求,本著“do not reinvent the wheel”的精神,建議使用一些第三方庫來滿足,比如:juju/errors。這里就不贅述了。

? 2015, bigwhite. 版權所有.

來自:http://tonybai.com/2015/10/30/error-handling-in-go/

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!