[譯]Go如何優雅的處理異常
原文:https://hackernoon.com/golang-handling-errors-gracefully-8e27f1db729f
<font color=#FF0000>注:譯文中error
可以理解為異常,但 Go 中的error
和 Java 中的異常還是有很大區別的,需要讀者慢慢體會,所以為了方便閱讀和思考,譯文中的名詞error
就不翻譯了。</font>
正文
  Go 有一套簡單的error
處理模型,但其實並不像看起來那麼簡單。本文中,我會提供一種好的方法去處理error
,並用這個方法來解決在往後程式設計遇到的的類似問題。
  首先,我們會分析下 Go 中的error
。
  接著我們來看看error
的產生和error
的處理,再分析其中的缺陷。
  最後,我們將要探索一種方法來解決我們在程式中遇到的類似問題。
什麼是 error?
  看下error
在內建包中的定義,我們可以得出一些結論:
// error型別在內建包中的定義是一個簡單的介面
// 其中nil代表沒有異常
type error interface {
Error() string
}
  從上面的程式碼,我們可以看到error
是一個介面,只有一個Error
方法。
  那我們要實現error
就很簡單了,看以下程式碼:
type MyCustomError string
func (err MyCustomError) Error() string {
return string(err)
}
  下面我們用標準包fmt
和errors
去宣告一些error
:
import (
"errors"
"fmt"
)
simpleError := errors.New("a simple error")
simpleError2 := fmt.Errorf("an error from a %s string", "formatted")
  思考:上面的error
定義中,只有這些簡單的資訊,就足夠處理好異常嗎?我們先不著急回答,下面我們去尋找一種好的解決方法。
error 處理流
  現在我們已經知道了在 Go 中的error
是怎樣的了,下一步我們來看下error
的處理流程。
  為了遵循簡約和 DRY(避免重複程式碼)原則,我們應該只在一個地方進行error
的處理。
  我們來看下以下的例子:
// 同時進行error處理和返回error
// 這是一種糟糕的寫法
func someFunc() (Result, error) {
result, err := repository.Find(id)
if err != nil {
log.Errof(err)
return Result{}, err
}
return result, nil
}
  上面這段程式碼有什麼問題呢?
  我們首先列印了這個error
資訊,然後又將error
返回給函式的呼叫者,這相當於重複進行了兩次error
處理。
  很有可能你組裡的同事會用到這個方法,當出現error
時,他很有可能又會將這個error
列印一遍,然後重複的日誌就會出現在系統日誌裡了。
  我們先假設程式有 3 層結構,分別是資料層,互動層和介面層:
// 資料層使用了一個第三方orm庫
func getFromRepository(id int) (Result, error) {
result := Result{ID: id}
err := orm.entity(&result)
if err != nil {
return Result{}, err
}
return result, nil
}
  根據 DRY 原則,我們可以將error
返回給呼叫的最上層介面層,這樣我們就能統一的對error
進行處理了。
  但上面的程式碼有一個問題,Go 的內建error
型別是沒有呼叫棧的。另外,如果error
產生在第三方庫中,我們還需要知道我們專案中的哪段程式碼負責了這個error
。
  github.com/pkg/errors 可以使用這個庫來解決上面的問題。
  利用這個庫,我對上面的程式碼進行了一些改進,加入了呼叫棧和加了一些相關的錯誤資訊。
import "github.com/pkg/errors"
// 使用了第三方的orm庫
func getFromRepository(id int) (Result, error) {
result := Result{ID: id}
err := orm.entity(&result)
if err != nil {
return Result{}, errors.Wrapf(err, "error getting the result with id %d", id);
}
return result, nil
}
// 當error封裝完後,返回的error資訊將會是這樣的
// err.Error() -> error getting the result with id 10
// 這就很容易知道這是來自orm庫的error了
  上面的程式碼對 orm 的error
進行了封裝,增加了呼叫棧,而且沒有修改原始的error
資訊。
   然後我們再來看看在其他層是如何處理這個error
的,首先是互動層:
func getInteractor(idString string) (Result, error) {
id, err := strconv.Atoi(idString)
if err != nil {
return Result{}, errors.Wrapf(err, "interactor converting id to int")
}
return repository.getFromRepository(id)
}
  接著是介面層:
func ResultHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
result, err := interactor.getInteractor(vars["id"])
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())
}
   現在我們只在最上層介面層處理了error
,看起來很完美?並不是,如果程式中經常返回 HTTP 錯誤碼 500,同時將錯誤列印到日誌中,像result not found
這種沒用的日誌就會很煩人。
解決方法
   我們上面討論到僅僅靠一個字串是不足以處理好 error 的。我們也知道通過給 error 加一些額外的資訊就能追溯到 error 的產生和最後的處理邏輯。
   因此我定義了三個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}
}
   從上面的程式碼可以看到,我們可以建立一個新的error
型別或者對已有的error
進行封裝。但我們遺漏了兩件事情,一是我們不知道error
的具體型別。二是我們不知道怎麼給這這個error
加上下文資訊。
   為了解決以上問題,我們來對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{"field": customErr.context.Field, "message": 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 "github.com/our_user/our_project/errors"
// The repository uses an external depedency orm
func getFromRepository(id int) (Result, error) {
result := Result{ID: id}
err := orm.entity(&result)
if err != nil {
msg := fmt.Sprintf("error getting the result with id %d", 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() -> 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, "interactor converting id to int")
err = errors.AddContext(err, "id", "wrong id format, should be an integer")
return Result{}, err
}
return repository.getFromRepository(id)
}
func ResultHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
result, err := interactor.getInteractor(vars["id"])
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,"error %s", err.Error())
errorContext := errors.GetContext(err)
if errorContext != nil {
fmt.Printf(w, "context %v", errorContext)
}
}
   通過簡單的封裝,我們可以明確的知道error
的錯誤型別了,然後我們就能方便進行處理了。
   讀者也可以將程式碼執行一遍,或者利用上面的errors
庫寫一些 demo 來加深理解。
感謝閱讀,歡迎大家指正,留言交流~
- 加微信實戰群請加微信(註明:實戰群):gocnio
相關文章
- 如何優雅的處理異常
- 如何優雅處理前端異常?前端
- 如何優雅地處理前端異常?前端
- 教你如何優雅處理Golang中的異常Golang
- java優雅的處理程式中的異常Java
- Egg優雅的實現異常處理
- 優雅的處理Spring Boot異常資訊Spring Boot
- SpringBoot優雅的全域性異常處理Spring Boot
- Spring Boot優雅地處理404異常Spring Boot
- SpringBoot進行優雅的全域性異常處理Spring Boot
- spring cloud優雅的處理feign熔斷異常SpringCloud
- 優雅地處理異常真是一門學問啊!
- 如何優雅的設計Java異常Java
- Go 語言異常處理Go
- 翻譯 | Java流中如何處理異常Java
- 如何優雅的關閉Go Channel「譯」Go
- SpringBoot介面 - 如何優雅的寫Controller並統一異常處理?Spring BootController
- async/await 如何優美的處理異常?AI
- JSP 異常處理如何處理?JS
- 異常處理 - Go 學習記錄Go
- [翻譯]-異常處理最佳實踐
- 異常的處理
- 異常篇——異常處理
- 異常-throws的方式處理異常
- [ gev ] Go 語言優雅處理 TCP “粘包”GoTCP
- Go語言之Goroutine與通道、異常處理Go
- .NET中異常處理的最佳實踐(譯)
- 異常處理
- 程式中的敏感資訊如何優雅的處理?
- 如何在 Go 中優雅的處理和返回錯誤(1)——函式內部的錯誤處理Go函式
- 如何優雅的在 koa 中處理錯誤
- gRPC 中的異常該如何處理?RPC
- 異常處理與異常函式函式
- 前端如何優雅處理類陣列物件?前端陣列物件
- 如何使用SpringMvc處理Rest異常SpringMVCREST
- 請問EJB容器如何處理異常
- JavaScript 異常處理JavaScript
- ThinkPHP 異常處理PHP