寫錯誤也要優雅,必須優雅!go語言nil的漂亮用法

引言

儘管go有一個簡單的錯誤模型,但乍一看,事情並不像它們應該的那樣簡單。在這篇文章中,我想提供一個很好的策略來處理錯誤並克服您在過程中可能遇到的問題。

首先,我們將分析go中的error。

然後我們將看到錯誤創建和錯誤處理之間的流程,並分析可能的缺陷。

最後探索一種解決方案,允許我們在不影響應用程序設計的情況下克服這些缺陷。

寫錯誤也要優雅,必須優雅!go語言nil的漂亮用法

error

不語言中的錯誤類型是什麼呢?下面是定義我們看一下。

<code> 
 

type

error

interface

{

Error

()

string

}/<code>

我們看到error是一個接口,它實現了一個返回字符串的簡單方法error。

這個定義告訴我們錯誤就是一個簡單的字符串,所以我們創建下面的結構。

<code>

type

MyCustomError

string

func

(err MyCustomError)

Error

()

string

{

return

string

(err) }/<code>

那要是這樣的話,我想到一個簡單的錯誤定義。

注意:這只是舉個例子。我們可以創建一個錯誤使用go標準包fmt和errors:

<code>

import

(

"errors"

"fmt"

) simpleError := errors.New(

"a simple error"

) simpleError2 := fmt.Errorf(

"an error from a %s string"

,

"formatted"

)/<code>

這個錯誤處理的寫法是不是很優雅?很簡單。在本文的最後,我們將深入的探討這個問題。


錯誤流處理

上面一小几節,我們已經知道什麼是錯誤。下一步是可視化生命週期中的錯誤流程。

為了簡單期間不要重複寫累贅的代碼。我們把錯誤處理抽象出來。

<code>

func

someFunc

()

(Result, error)

{ result, err := repository.Find(id)

if

err !=

nil

{ log.Errof(err)

return

Result{}, err }

return

result,

nil

}/<code>

上面這段代碼的錯誤處理有什麼不妥之處嗎?原來我們通過首先記錄錯誤,然後又返回錯誤,處理了兩次。

試想如果團隊開發,你的隊友調用了這個錯誤處理函數,然後又手動的打印錯誤日誌。這是不是糟糕極了?


假如我們的應用有3層,repository - interactor - web server,看下面的代碼:

<code>

func

getFromRepository

(id

int

)

(Result, error)

{ result := Result{ID: id} err := orm.entity(&result)

if

err !=

nil

{

return

Result{}, err }

return

result,

nil

}/<code>

先是處理邏輯,然後從數據庫拿數據。如果獲取數據失敗,返回故障信息。如果獲取數據正常,直接返回數據。這是通常的做法,也是一種很成熟和穩定的方法。

上面的代碼雖然邏輯上很合理。但是也有一個問題。go語言的錯誤處理沒有堆棧跟蹤,所以如果拋出異常,我們無法追蹤到底是哪一行發生的錯誤。

pkg/errors庫彌補了這個不足。

接著改進上面的代碼。我們明確的指定錯誤拋出位置的信息。

<code>

import

"github.com/pkg/errors"

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

}/<code>

經過這樣處理後,發生錯誤時返回的信息如下。

<code>/<code>

這個函數的作用,就是封裝來自ORM的錯誤,在不影響原始信息的情況下,添加了堆棧跟蹤的功能。

在interactor層的用法:

<code>

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) }/<code>

頂層web server的用法:

<code>r := mux.NewRouter()
r.HandleFunc(

"/result/{id}"

, ResultHandler)

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)

{ w.WriteHeader(http.StatusIntervalServerError) log.Errorf(err) fmt.Fprintf(w, err.Error()) }/<code>

大家看在頂層處理錯誤,完美嗎?不完美。為什麼呢?因為都是一些500的HTTP CODE,沒什麼用,給日誌文件添加的都是無用的數據。

優雅的用法

上一段您也看到了,在web server層處理錯誤,不完美啊,都混沌了。

我們知道,如果我們在錯誤中引入新的內容,我們將以某種方式在創建錯誤的地方和最終處理錯誤的時候引入依賴項。

所以讓我們來探索一個定義3個目標的解決方案:

  1. 提供良好的錯誤堆棧跟蹤
  2. web層面的錯誤日誌
  3. 必要時為用戶提供上下文錯誤信息。(例如:所提供的電子郵件格式不正確)

首先創建一個錯誤類型。

<code>

package

errors

const

( NoType = ErrorType(

iota

) BadRequest NotFound )

type

ErrorType

uint

type

customError

struct

{ errorType ErrorType originalError error contextInfo

map

[

string

]

string

}

func

(error customError)

Error

()

string

{

return

error.originalError.Error() }

func

(

type

ErrorType)

New

(msg

string

)

error

{

return

customError{errorType:

type

, originalError: errors.New(msg)} }

func

(

type

ErrorType)

Newf

(msg

string

, args ...

interface

{})

error

{ err := fmt.Errof(msg, args...)

return

customError{errorType:

type

, originalError: err} }

func

(

type

ErrorType)

Wrap

(err error, msg

string

)

error

{

return

type

.Wrapf(err, msg) }

func

(

type

ErrorType)

Wrapf

(err error, msg

string

, args ...

interface

{})

error

{ newErr := errors.Wrapf(err, msg, args..)

return

customError{errorType: errorType, originalError: newErr} }/<code>

正如上面代碼所示,只有ErrorType 和錯誤類型是公開可訪問的。我們可以創建任意新的錯誤,或修飾已存在的錯誤。

但是有兩件事情沒有做到:

  • 如何在不導出customError的情況下檢查錯誤類型?
  • 我們如何向錯誤中添加/獲取上下文,甚至是向外部依賴項中已存在的錯誤中添加上下文?

改進上面的代碼:

<code>

func

New

(msg

string

)

error

{

return

customError{errorType: NoType, originalError: errors.New(msg)} }

func

Newf

(msg

string

, args ...

interface

{})

error

{

return

customError{errorType: NoType, originalError: errors.New(fmt.Sprintf(msg, args...))} }

func

Wrap

(err error, msg

string

)

error

{

return

Wrapf(err, msg) }

func

Cause

(err error)

error

{

return

errors.Cause(err) }

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} }/<code>

現在讓我們建立我們的方法處理上下文和任何一般錯誤的類型:

<code>

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} }

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

}

func

GetType

(err error)

ErrorType

{

if

customErr, ok := err.(customError); ok {

return

customErr.errorType }

return

NoType }/<code>

現在回到我們的例子,我們要應用這個新的錯誤包:

<code>

import

"github.com/our_user/our_project/errors"

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

}/<code>

interactor層的寫法:

<code>

func

getInteractor(idString string) (Result, error) {

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)

}

/<code>

最後web server層的寫法:

<code>r := mux.NewRouter()
r.HandleFunc(

"/result/{id}"

, ResultHandler)

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) } }/<code>

寫在最後

大家看到了,使用導出的類型和一些導出的值,我們可以更輕鬆地處理錯誤。

這個解決方案在創建錯誤時,也顯式地顯示了錯誤的類型,這很贊!


分享到:


相關文章: