Linux中國

我對 Go 的錯誤處理有哪些不滿,以及我是如何處理的

Go 的人往往對它的錯誤處理模式有一定的看法。按不同的語言經驗,人們可能有不同的習慣處理方法。這就是為什麼我決定要寫這篇文章,儘管有點固執己見,但我認為聽取我的經驗是有用的。我想要講的主要問題是,很難去強制執行良好的錯誤處理實踐,錯誤經常沒有堆棧追蹤,並且錯誤處理本身太冗長。不過,我已經看到了一些潛在的解決方案,或許能幫助解決一些問題。

與其他語言的快速比較

在 Go 中,所有的錯誤都是值。因為這點,相當多的函數最後會返回一個 error, 看起來像這樣:

func (s *SomeStruct) Function() (string, error)

因此這導致調用代碼通常會使用 if 語句來檢查它們:

bytes, err := someStruct.Function()
if err != nil {
  // Process error
}

另外一種方法,是在其他語言中,如 Java、C#、Javascript、Objective C、Python 等使用的 try-catch 模式。如下你可以看到與先前的 Go 示例類似的 Java 代碼,聲明 throws 而不是返回 error

public String function() throws Exception

它使用的是 try-catch 而不是 if err != nil

try {
  String result = someObject.function()
  // continue logic
}
catch (Exception e) {
  // process exception
}

當然,還有其他的不同。例如,error 不會使你的程序崩潰,然而 Exception 會。還有其他的一些,在本篇中會專門提到這些。

實現集中式錯誤處理

退一步,讓我們看看為什麼要在一個集中的地方處理錯誤,以及如何做到。

大多數人或許會熟悉的一個例子是 web 服務 - 如果出現了一些未預料的的服務端錯誤,我們會生成一個 5xx 錯誤。在 Go 中,你或許會這麼實現:

func init() {
    http.HandleFunc("/users", viewUsers)
    http.HandleFunc("/companies", viewCompanies)
}

func viewUsers(w http.ResponseWriter, r *http.Request) {
    user // some code
    if err := userTemplate.Execute(w, user); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

func viewCompanies(w http.ResponseWriter, r *http.Request) {
    companies = // some code
    if err := companiesTemplate.Execute(w, companies); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

這並不是一個好的解決方案,因為我們不得不重複地在所有的處理函數中處理錯誤。為了能更好地維護,最好能在一處地方處理錯誤。幸運的是,在 Go 語言的官方博客中,Andrew Gerrand 提供了一個替代方法,可以完美地實現。我們可以創建一個處理錯誤的 Type:

type appHandler func(http.ResponseWriter, *http.Request) error

func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if err := fn(w, r); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

這可以作為一個封裝器來修飾我們的處理函數:

func init() {
    http.Handle("/users", appHandler(viewUsers))
    http.Handle("/companies", appHandler(viewCompanies))
}

接著我們需要做的是修改處理函數的簽名來使它們返回 errors。這個方法很好,因為我們做到了 DRY 原則,並且沒有重複使用不必要的代碼 - 現在我們可以在單獨一個地方返回默認錯誤了。

錯誤上下文

在先前的例子中,我們可能會收到許多潛在的錯誤,它們中的任何一個都可能在調用堆棧的許多環節中生成。這時候事情就變得棘手了。

為了演示這點,我們可以擴展我們的處理函數。它可能看上去像這樣,因為模板執行並不是唯一一處會發生錯誤的地方:

func viewUsers(w http.ResponseWriter, r *http.Request) error {
    user, err := findUser(r.formValue("id")) 
    if err != nil {
      return err;
    }
    return userTemplate.Execute(w, user);
}

調用鏈可能會相當深,在整個過程中,各種錯誤可能在不同的地方實例化。Russ Cox的這篇文章解釋了如何避免遇到太多這類問題的最佳實踐:

「在 Go 中錯誤報告的部分約定是函數包含相關的上下文,包括正在嘗試的操作(比如函數名和它的參數)。」

這個給出的例子是對 OS 包的一個調用:

err := os.Remove("/tmp/nonexist")
fmt.Println(err)

它會輸出:

remove /tmp/nonexist: no such file or directory

總結一下,執行後,輸出的是被調用的函數、給定的參數、特定的出錯信息。當在其他語言中創建一個 Exception 消息時,你也可以遵循這個實踐。如果我們在 viewUsers 處理中堅持這點,那麼幾乎總是能明確錯誤的原因。

問題來自於那些不遵循這個最佳實踐的人,並且你經常會在第三方的 Go 庫中看到這些消息:

Oh no I broke

這沒什麼幫助 - 你無法了解上下文,這使得調試很困難。更糟糕的是,當這些錯誤被忽略或返回時,這些錯誤會被備份到堆棧中,直到它們被處理為止:

if err != nil {
  return err
}

這意味著錯誤何時發生並沒有被傳遞出來。

應該注意的是,所有這些錯誤都可以在 Exception 驅動的模型中發生 - 糟糕的錯誤信息、隱藏異常等。那麼為什麼我認為該模型更有用?

即便我們在處理一個糟糕的異常消息,我們仍然能夠了解它發生在調用堆棧中什麼地方。因為堆棧跟蹤,這引發了一些我對 Go 不了解的部分 - 你知道 Go 的 panic 包含了堆棧追蹤,但是 error 沒有。我推測可能是 panic 會使你的程序崩潰,因此需要一個堆棧追蹤,而處理錯誤並不會,因為它會假定你在它發生的地方做一些事。

所以讓我們回到之前的例子 - 一個有糟糕錯誤信息的第三方庫,它只是輸出了調用鏈。你認為調試會更容易嗎?

panic: Oh no I broke
[signal 0xb code=0x1 addr=0x0 pc=0xfc90f]

goroutine 1103 [running]:
panic(0x4bed00, 0xc82000c0b0)
/usr/local/go/src/runtime/panic.go:481 +0x3e6
github.com/Org/app/core.(_app).captureRequest(0xc820163340, 0x0, 0x55bd50, 0x0, 0x0)
/home/ubuntu/.go_workspace/src/github.com/Org/App/core/main.go:313 +0x12cf
github.com/Org/app/core.(_app).processRequest(0xc820163340, 0xc82064e1c0, 0xc82002aab8, 0x1)
/home/ubuntu/.go_workspace/src/github.com/Org/App/core/main.go:203 +0xb6
github.com/Org/app/core.NewProxy.func2(0xc82064e1c0, 0xc820bb2000, 0xc820bb2000, 0x1)
/home/ubuntu/.go_workspace/src/github.com/Org/App/core/proxy.go:51 +0x2a
github.com/Org/app/core/vendor/github.com/rusenask/goproxy.FuncReqHandler.Handle(0xc820da36e0, 0xc82064e1c0, 0xc820bb2000, 0xc5001, 0xc820b4a0a0)
/home/ubuntu/.go_workspace/src/github.com/Org/app/core/vendor/github.com/rusenask/goproxy/actions.go:19 +0x30

我認為這可能是 Go 的設計中被忽略的東西 - 不是所有語言都不會忽視的。

如果我們使用 Java 作為一個隨意的例子,其中人們犯的一個最愚蠢的錯誤是不記錄堆棧追蹤:

LOGGER.error(ex.getMessage()) // 不記錄堆棧追蹤
LOGGER.error(ex.getMessage(), ex) // 記錄堆棧追蹤

但是 Go 似乎在設計中就沒有這個信息。

在獲取上下文信息方面 - Russ 還提到了社區正在討論一些潛在的介面用於剝離上下文錯誤。關於這點,了解更多或許會很有趣。

堆棧追蹤問題解決方案

幸運的是,在做了一些查找後,我發現了這個出色的 Go 錯誤庫來幫助解決這個問題,來給錯誤添加堆棧跟蹤:

if errors.Is(err, crashy.Crashed) {
  fmt.Println(err.(*errors.Error).ErrorStack())
}

不過,我認為這個功能如果能成為語言的 第一類公民 first class citizenship 將是一個改進,這樣你就不必做一些類型修改了。此外,如果我們像先前的例子那樣使用第三方庫,它可能沒有使用 crashy - 我們仍有相同的問題。

我們對錯誤應該做什麼?

我們還必須考慮發生錯誤時應該發生什麼。這一定有用,它們不會讓你的程序崩潰,通常也會立即處理它們:

err := method()
if err != nil {
  // some logic that I must do now in the event of an error!
}

如果我們想要調用大量方法,它們會產生錯誤,然後在一個地方處理所有錯誤,這時會發生什麼?看上去像這樣:

err := doSomething()
if err != nil {
  // handle the error here
}

func doSomething() error {
  err := someMethod()
  if err != nil {
    return err
  }
  err = someOther()
  if err != nil {
    return err
  }
  someOtherMethod()
}

這感覺有點冗餘,在其他語言中你可以將多條語句作為一個整體處理。

try {
  someMethod()
  someOther()
  someOtherMethod()
}
catch (Exception e) {
  // process exception
}

或者只要在方法簽名中傳遞錯誤:

public void doSomething() throws SomeErrorToPropogate {
  someMethod()
  someOther()
  someOtherMethod()
}

我個人認為這兩個例子實現了一件事情,只是 Exception 模式更少冗餘,更加彈性。如果有什麼的話,我覺得 if err!= nil 感覺像樣板。也許有一種方法可以清理?

將失敗的多條語句做為一個整體處理錯誤

首先,我做了更多的閱讀,並在 Rob Pike 寫的 Go 博客中發現了一個比較務實的解決方案。

他定義了一個封裝了錯誤的方法的結構體:

type errWriter struct {
    w   io.Writer
    err error
}

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

讓我們這麼做:

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}

這也是一個很好的方案,但是我感覺缺少了點什麼 - 因為我們不能重複使用這個模式。如果我們想要一個含有字元串參數的方法,我們就不得不改變函數簽名。或者如果我們不想執行寫操作會怎樣?我們可以嘗試使它更通用:

type errWrapper struct {
    err error
}
func (ew *errWrapper) do(f func() error) {
    if ew.err != nil {
        return
    }
    ew.err = f();
}

但是我們有一個相同的問題,如果我們想要調用含有不同參數的函數,它就無法編譯了。然而你可以簡單地封裝這些函數調用:

w := &errWrapper{}

w.do(func() error {
    return someFunction(1, 2);
})

w.do(func() error {
    return otherFunction("foo");
})

err := w.err

if err != nil {
// process error here
}

這可以用,但是並沒有太大幫助,因為它最終比標準的 if err != nil 檢查帶來了更多的冗餘。如果有人能提供其他解決方案,我會很有興趣聽。或許這個語言本身需要一些方法來以不那麼臃腫的方式傳遞或者組合錯誤 - 但是感覺似乎是特意設計成不那麼做。

總結

看完這些之後,你可能會認為我在對 error 挑刺兒,由此推論我反對 Go。事實並非如此,我只是將它與我使用 try catch 模型的經驗進行比較。它是一個用於系統編程很好的語言,並且已經出現了一些優秀的工具。僅舉幾例,有 KubernetesDockerTerraformHoverfly 等。還有小型、高性能、本地二進位的優點。但是,error 難以適應。 我希望我的推論是有道理的,而且一些方案和解決方法可能會有幫助。

作者簡介:

Andrew 是 OpenCredo 的顧問,於 2015 年加入公司。Andrew 在多個行業工作多年,開發基於 Web 的企業應用程序。

via: https://opencredo.com/why-i-dont-like-error-handling-in-go

作者:Andrew Morgan 譯者:geekpi 校對:jasminepeng

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國