原文:hackernoon.com/golang-hand…git
注:譯文中error
能夠理解爲異常,但Go中的error
和Java中的異常仍是有很大區別的,須要讀者慢慢體會,因此爲了方便閱讀和思考,譯文中的名詞error
就不翻譯了。github
Go有一套簡單的error
處理模型,但其實並不像看起來那麼簡單。本文中,我會提供一種好的方法去處理error
,並用這個方法來解決在日後編程遇到的的相似問題。golang
首先,咱們會分析下Go中的error
。編程
接着咱們來看看error
的產生和error
的處理,再分析其中的缺陷。api
最後,咱們將要探索一種方法來解決咱們在程序中遇到的相似問題。markdown
看下error
在內置包中的定義,咱們能夠得出一些結論:app
// error類型在內置包中的定義是一個簡單的接口 // 其中nil表明沒有異常 type error interface { Error() string } 複製代碼
從上面的代碼,咱們能夠看到error
是一個接口,只有一個Error
方法。框架
那咱們要實現error
就很簡單了,看如下代碼:函數
type MyCustomError string func (err MyCustomError) Error() string { return string(err) } 複製代碼
下面咱們用標準包fmt
和errors
去聲明一些error
:oop
import ( "errors" "fmt" ) simpleError := errors.New("a simple error") simpleError2 := fmt.Errorf("an error from a %s string", "formatted") 複製代碼
思考:上面的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
類型:
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來加深理解。