[譯]Go如何優雅的處理異常

joy發表於2019-05-29

原文:https://hackernoon.com/golang-handling-errors-gracefully-8e27f1db729f

<font color=#FF0000>注:譯文中error可以理解為異常,但 Go 中的error和 Java 中的異常還是有很大區別的,需要讀者慢慢體會,所以為了方便閱讀和思考,譯文中的名詞error就不翻譯了。</font>

正文

&emsp;&emsp;Go 有一套簡單的error處理模型,但其實並不像看起來那麼簡單。本文中,我會提供一種好的方法去處理error,並用這個方法來解決在往後程式設計遇到的的類似問題。

&emsp;&emsp;首先,我們會分析下 Go 中的error

&emsp;&emsp;接著我們來看看error的產生和error的處理,再分析其中的缺陷。

&emsp;&emsp;最後,我們將要探索一種方法來解決我們在程式中遇到的類似問題。

什麼是 error?

&emsp;&emsp;看下error在內建包中的定義,我們可以得出一些結論:

// error型別在內建包中的定義是一個簡單的介面
// 其中nil代表沒有異常
type error interface {
    Error() string
}

&emsp;&emsp;從上面的程式碼,我們可以看到error是一個介面,只有一個Error方法。

&emsp;&emsp;那我們要實現error就很簡單了,看以下程式碼:

type MyCustomError string

func (err MyCustomError) Error() string {
  return string(err)
}

&emsp;&emsp;下面我們用標準包fmterrors去宣告一些error

import (
  &quot;errors&quot;
  &quot;fmt&quot;
)

simpleError := errors.New(&quot;a simple error&quot;)
simpleError2 := fmt.Errorf(&quot;an error from a %s string&quot;, &quot;formatted&quot;)

&emsp;&emsp;思考:上面的error定義中,只有這些簡單的資訊,就足夠處理好異常嗎?我們先不著急回答,下面我們去尋找一種好的解決方法。

error 處理流

&emsp;&emsp;現在我們已經知道了在 Go 中的error是怎樣的了,下一步我們來看下error的處理流程。

&emsp;&emsp;為了遵循簡約和 DRY(避免重複程式碼)原則,我們應該只在一個地方進行error的處理。

&emsp;&emsp;我們來看下以下的例子:

// 同時進行error處理和返回error
// 這是一種糟糕的寫法
func someFunc() (Result, error) {
    result, err := repository.Find(id)
    if err != nil {
        log.Errof(err)
        return Result{}, err
    }
    return result, nil
}

&emsp;&emsp;上面這段程式碼有什麼問題呢?

&emsp;&emsp;我們首先列印了這個error資訊,然後又將error返回給函式的呼叫者,這相當於重複進行了兩次error處理。

&emsp;&emsp;很有可能你組裡的同事會用到這個方法,當出現error時,他很有可能又會將這個error列印一遍,然後重複的日誌就會出現在系統日誌裡了。

&emsp;&emsp;我們先假設程式有 3 層結構,分別是資料層,互動層和介面層:

// 資料層使用了一個第三方orm庫
func getFromRepository(id int) (Result, error) {
    result := Result{ID: id}
    err := orm.entity(&amp;result)
    if err != nil {
        return Result{}, err
    }
    return result, nil 
}

&emsp;&emsp;根據 DRY 原則,我們可以將error返回給呼叫的最上層介面層,這樣我們就能統一的對error進行處理了。

&emsp;&emsp;但上面的程式碼有一個問題,Go 的內建error型別是沒有呼叫棧的。另外,如果error產生在第三方庫中,我們還需要知道我們專案中的哪段程式碼負責了這個error

&emsp;&emsp;github.com/pkg/errors 可以使用這個庫來解決上面的問題。

&emsp;&emsp;利用這個庫,我對上面的程式碼進行了一些改進,加入了呼叫棧和加了一些相關的錯誤資訊。

import &quot;github.com/pkg/errors&quot;

// 使用了第三方的orm庫
func getFromRepository(id int) (Result, error) {
    result := Result{ID: id}
    err := orm.entity(&amp;result)
    if err != nil {
        return Result{}, errors.Wrapf(err, &quot;error getting the result with id %d&quot;, id);
  }
  return result, nil 
}
// 當error封裝完後,返回的error資訊將會是這樣的
// err.Error() -&gt; error getting the result with id 10
// 這就很容易知道這是來自orm庫的error了

&emsp;&emsp;上面的程式碼對 orm 的error進行了封裝,增加了呼叫棧,而且沒有修改原始的error資訊。

&emsp;&emsp; 然後我們再來看看在其他層是如何處理這個error的,首先是互動層:

func getInteractor(idString string) (Result, error) {
  id, err := strconv.Atoi(idString)
  if err != nil {
    return Result{}, errors.Wrapf(err, &quot;interactor converting id to int&quot;)
  }
  return repository.getFromRepository(id)
}

&emsp;&emsp;接著是介面層:

func ResultHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    result, err := interactor.getInteractor(vars[&quot;id&quot;])
    if err != nil {
        handleError(w, err)
    }
    fmt.Fprintf(w, result)
}
func handleError(w http.ResponseWriter, err error) {
    // 返回HTTO 500錯誤
    w.WriteHeader(http.StatusIntervalServerError)
    log.Errorf(err)
    fmt.Fprintf(w, err.Error())
}

&emsp;&emsp; 現在我們只在最上層介面層處理了error,看起來很完美?並不是,如果程式中經常返回 HTTP 錯誤碼 500,同時將錯誤列印到日誌中,像result not found這種沒用的日誌就會很煩人。

解決方法

&emsp;&emsp; 我們上面討論到僅僅靠一個字串是不足以處理好 error 的。我們也知道通過給 error 加一些額外的資訊就能追溯到 error 的產生和最後的處理邏輯。

&emsp;&emsp; 因此我定義了三個error處理的宗旨。

error 處理的三個宗旨

  • 提供清晰完整的呼叫棧
  • 必要時提供 error 的上下文資訊
  • 列印 error 到日誌中(例如可以在框架層列印)

我們來建立一個error型別:

const(
  NoType = ErrorType(iota)
  BadRequest
  NotFound
  // 可以加入你需要的error型別
)

type ErrorType uint

type customError struct {
  errorType ErrorType
  originalError error
  contextInfo map[string]string 
}

// 返回customError具體的錯誤資訊
func (error customError) Error() string {
   return error.originalError.Error()
}

// 建立一個新的customError
func (type ErrorType) New(msg string) error {
   return customError{errorType: type, originalError: errors.New(msg)}
}

// 給customError自定義錯誤資訊
func (type ErrorType) Newf(msg string, args ...interface{}) error {
   err := fmt.Errof(msg, args...)

   return customError{errorType: type, originalError: err}
}

// 對error進行封裝
func (type ErrorType) Wrap(err error, msg string) error {
   return type.Wrapf(err, msg)
}

// 對error進行封裝,並加入格式化資訊
func (type ErrorType) Wrapf(err error, msg string, args ...interface{}) error {
   newErr := errors.Wrapf(err, msg, args..)

   return customError{errorType: errorType, originalError: newErr}
}

&emsp;&emsp; 從上面的程式碼可以看到,我們可以建立一個新的error型別或者對已有的error進行封裝。但我們遺漏了兩件事情,一是我們不知道error的具體型別。二是我們不知道怎麼給這這個error加上下文資訊。

&emsp;&emsp; 為了解決以上問題,我們來對github.com/pkg/errors的方法也進行一些封裝。

// 建立一個NoType error
func New(msg string) error {
   return customError{errorType: NoType, originalError: errors.New(msg)}
}

// 建立一個加入了格式化資訊的NoType error
func Newf(msg string, args ...interface{}) error {
   return customError{errorType: NoType, originalError: errors.New(fmt.Sprintf(msg, args...))}
}

// 給error封裝多一層string
func Wrap(err error, msg string) error {
   return Wrapf(err, msg)
}

// 返回最原始的error
func Cause(err error) error {
   return errors.Cause(err)
}

// error加入格式化資訊
func Wrapf(err error, msg string, args ...interface{}) error {
   wrappedError := errors.Wrapf(err, msg, args...)
   if customErr, ok := err.(customError); ok {
      return customError{
         errorType: customErr.errorType,
         originalError: wrappedError,
         contextInfo: customErr.contextInfo,
      }
   }

   return customError{errorType: NoType, originalError: wrappedError}
}

接著我們給error加入上下文資訊:

// AddErrorContext adds a context to an error
func AddErrorContext(err error, field, message string) error {
   context := errorContext{Field: field, Message: message}
   if customErr, ok := err.(customError); ok {
      return customError{errorType: customErr.errorType, originalError: customErr.originalError, contextInfo: context}
   }

   return customError{errorType: NoType, originalError: err, contextInfo: context}
}

// GetErrorContext returns the error context
func GetErrorContext(err error) map[string]string {
   emptyContext := errorContext{}
   if customErr, ok := err.(customError); ok || customErr.contextInfo != emptyContext  {

      return map[string]string{&quot;field&quot;: customErr.context.Field, &quot;message&quot;: customErr.context.Message}
   }

   return nil
}

// GetType returns the error type
func GetType(err error) ErrorType {
   if customErr, ok := err.(customError); ok {
      return customErr.errorType
   }

   return NoType
}

現在將上述的方法應用在我們文章開頭寫的 example 中:

import &quot;github.com/our_user/our_project/errors&quot;
// The repository uses an external depedency orm
func getFromRepository(id int) (Result, error) {
    result := Result{ID: id}
    err := orm.entity(&amp;result)
    if err != nil {
        msg := fmt.Sprintf(&quot;error getting the  result with id %d&quot;, id)
        switch err {
        case orm.NoResult:
            err = errors.Wrapf(err, msg);
        default:
            err = errors.NotFound(err, msg);
        }
        return Result{}, err
    }
    return result, nil 
}
// after the error wraping the result will be
// err.Error() -&gt; error getting the result with id 10: whatever it comes from the orm
func getInteractor(idString string) (Result, error) {
    id, err := strconv.Atoi(idString)
    if err != nil {
        err = errors.BadRequest.Wrapf(err, &quot;interactor converting id to int&quot;)
        err = errors.AddContext(err, &quot;id&quot;, &quot;wrong id format, should be an integer&quot;)
        return Result{}, err
    }
    return repository.getFromRepository(id)
}


func ResultHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    result, err := interactor.getInteractor(vars[&quot;id&quot;])
    if err != nil {
        handleError(w, err)
    }
    fmt.Fprintf(w, result)
}
func handleError(w http.ResponseWriter, err error) {
    var status int
    errorType := errors.GetType(err)
    switch errorType {
    case BadRequest:
        status = http.StatusBadRequest
    case NotFound:
        status = http.StatusNotFound
    default:
        status = http.StatusInternalServerError
    }
    w.WriteHeader(status)

    if errorType == errors.NoType {
        log.Errorf(err)
    }
    fmt.Fprintf(w,&quot;error %s&quot;, err.Error())

    errorContext := errors.GetContext(err)
    if errorContext != nil {
        fmt.Printf(w, &quot;context %v&quot;, errorContext)
   }
}

&emsp;&emsp; 通過簡單的封裝,我們可以明確的知道error的錯誤型別了,然後我們就能方便進行處理了。

&emsp;&emsp; 讀者也可以將程式碼執行一遍,或者利用上面的errors庫寫一些 demo 來加深理解。

感謝閱讀,歡迎大家指正,留言交流~

更多原創文章乾貨分享,請關注公眾號
  • [譯]Go如何優雅的處理異常
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章