對 echo 框架進行統一的自定義錯誤處理

Muninn發表於2017-04-11

藉助移動端的增長,如今 RESTful 風格的 API 已經十分流行,
用各種語言去寫後端 API 都有很成熟方便的方案,用 golang 寫後端 API 更是生產力的代表,
你可以用不輸 python/ruby 這類動態語言的速度,寫出效能高出一兩個數量級的後端 API 。

ECHO 框架

由於 golang 的標準庫在網路方面已經很完善,導致框架發揮餘地不大。很多高手都說,
用什麼框架,用標準庫就寫好了,框架只是語法糖而已,還會限制專案的發展。
不過我們並不是高手,語法糖也是糖,用一個趁手的框架還是能提高不少效率的。
要是在半年前,你讓我推薦框架,我會說有很多,都各有優缺點,除了 beego 隨便選一個就可以。
但是來到2017年,一個叫 Echo 的框架脫穎而出。這是我目前最推薦的框架。
Echo 的宣傳語用的是 “高效能,易擴充套件,極簡 Go Web 框架” 。它的一些特性如下圖所示:

對 echo 框架進行統一的自定義錯誤處理
Echo Features

這些特性裡,HTTP/2,Auto HTTPS,聽著很熟?這是我之前介紹的 Caddy 也有的特性,
因為 golang 實現這些太容易了。還有 Middleware 裡的一大堆功能也差不多。
我們在做微服務的時候,這些通用的東西由 API Gateway 統一實現就好了,
如果你寫的是個小的獨立應用的後端,這些開箱即用的功能倒是能提供很大的幫助。

其實今天我主要想說說最後一個特性裡提到的,“中心化的 HTTP 錯誤處理”。

RESTful API 錯誤返回

一個團隊應當有一份 RESTful API 的規範,而在規範中應該規範響應格式,包括所有錯誤響應的格式。
比如微軟的規範
jsonapi.org 推薦規範等等。
大部分時候我們不需要實現的那麼繁瑣,我們規定一個簡單的結構:

STATUS 400 Bad Request
{
  "error": "InvalidID",
  "message": "invalid id in your url query parameters"
}複製程式碼

傳統的錯誤響應可能只有一個伴隨 HTTP Status code 的 string 型別的 message,
如今我們把正常的響應格式變成了 JSON ,那麼把錯誤返回也用 JSON 吧。
除了用 JSON 之外,我們又增加了一個 error 欄位,
這個欄位是一個比 Status code 要詳細一個級別的 Key,
消費端可以用這個約定的 Key 做更為靈活的錯誤處理。

好了,我們就用這個簡單的例子進行下去,今天主題講的是 Echo 去統一處理的方法。

Echo 怎麼統一處理錯誤?

其實 Echo 的文件雖然很漂亮,但是不夠詳細,深入一點的內容和例子並沒有。
但一個漂亮的 golang 專案,程式碼即是文件,我們應該有去 godoc.org 查文件的習慣。
我們找到 Echo 的 GoDoc
看 Echo 型別:

type Echo struct {
    Server           *http.Server
    TLSServer        *http.Server
    Listener         net.Listener
    TLSListener      net.Listener
    DisableHTTP2     bool
    Debug            bool
    HTTPErrorHandler HTTPErrorHandler
    Binder           Binder
    Validator        Validator
    Renderer         Renderer
    AutoTLSManager   autocert.Manager
    Mutex            sync.RWMutex
    Logger           Logger
    // contains filtered or unexported fields
}複製程式碼

果然可以定義 HTTPErrorHandler, 順著找過去,

// HTTPErrorHandler is a centralized HTTP error handler.
type HTTPErrorHandler func(error, Context)複製程式碼

它是一個傳入 error 和 Context 並且沒有返回值的函式。
可是知道這些還是有點暈?並不知道怎麼寫這個函式啊。
沒關係,我這篇文章就是講怎麼寫這個函式的。往下看吧。

定義錯誤結構

由於 golang 是靜態型別,我們幹啥都需要先定義個結構,程式碼如下:

type httpError struct {
    code    int
    Key     string `json:"error"`
    Message string `json:"message"`
}

func newHTTPError(code int, key string, msg string) *httpError {
    return &httpError{
        code:    code,
        Key:     key,
        Message: msg,
    }
}

// Error makes it compatible with `error` interface.
func (e *httpError) Error() string {
    return e.Key + ": " + e.Message
}複製程式碼

這裡我們做了三件事

  1. 定義了錯誤的結構,其中包含 code,key 和 message,key 和 message 可以被匯出為 JSON。
  2. 做了個新建錯誤結構的函式,這樣就可以用一行程式碼去新建一個錯誤了。
  3. 給這個結構增加了 Error 函式,這樣這個結構就成了一個 golang 的 error 介面。

處理錯誤

我們終於可以寫上文提到的自定義函式了,先看示例程式碼我再做解釋,然後你就能寫自己的了:

package main

import (
    "net/http"

    "github.com/labstack/echo"
)

// httpErrorHandler customize echo's HTTP error handler.
func httpErrorHandler(err error, c echo.Context) {
    var (
        code = http.StatusInternalServerError
        key  = "ServerError"
        msg  string
    )

    if he, ok := err.(*httpError); ok {
        code = he.code
        key = he.Key
        msg = he.Message
    } else if config.Debug {
        msg = err.Error()
    } else {
        msg = http.StatusText(code)
    }

    if !c.Response().Committed {
        if c.Request().Method == echo.HEAD {
            err := c.NoContent(code)
            if err != nil {
                c.Logger().Error(err)
            }
        } else {
            err := c.JSON(code, newHTTPError(code, key, msg))
            if err != nil {
                c.Logger().Error(err)
            }
        }
    }
}複製程式碼

這個函式的功能就是根據傳進來的 error 和上下文 Context,組裝出合適的 HTTP 響應。
可因為 golang 的 error 是一個介面,也就是第一個引數可能傳進來任何奇怪的東西,
我們需要細心的處理一下。

第一部分我們定義了預設值作為最壞的情況,在 HTTP API 裡,消費端要是看到這種最壞的情況,
說明你要被扣獎金了,除非你可以甩鍋給你依賴的模組或基礎設施。

第二部分我們先看看傳進來的錯誤是不是我們之前定義的,如果是那就太好了。如果不是的話,
看來是一個其他的未知錯誤,如果 Debug 開著,那還好,不用扣獎金,我們把錯誤明細直接返回
到 msg 裡方便除錯。如果也沒開 Debug ... 那隻好硬著頭皮返回 500 並什麼資訊都不給了。

第三部分你可以基本照抄,是檢查上下文中是否宣告這個響應已經提交了,只有沒提交的時候,
我們才需要把我們準備好的錯誤資訊以 JSON 格式提交,順便列印錯誤日誌。另外,如果請求
是 HEAD 方法的話,根據規範,你只能返回狀態 204 並默默在日誌記錄錯誤了。

應用

好了,我們寫好了統一的錯誤處理,該怎麼使用呢? 來看一個極簡的例子吧:

func getUser(c echo.Context) error {
    var u user
    id := c.Param("id")
    if !bson.IsObjectIdHex(id) {
        return newHTTPError(http.StatusBadRequest, "InvalidID", "invalid user id")
    }
    err := db.C("user").FindId(bson.ObjectIdHex(id)).One(&u)
    if err == mgo.ErrNotFound {
        return newHTTPError(http.StatusNotFound, "NotFound", err.Error())
    }
    if err != nil {
        return err
    }
    return c.JSON(http.StatusOK, u)
}複製程式碼

這是個從 mongodb 取 user 的例子,

  1. 檢查url中的id是不是一個合法的id,不是的話,返回我們之前自定義的錯誤。
  2. 去資料庫裡查,如果沒有記錄,返回 404 錯誤。
  3. 如果查詢資料庫的操作出了其他錯誤,這個時候我們無能為力了,只好直接把這個錯誤返回。
  4. 一切正常沒錯誤的話,我們返回狀態 200 和 JSON 資料。

我們可以看出,經過這麼一番折騰,在寫API的時候,省心了很多。
我們可以隨手用一行程式碼構造錯誤,也可以直接把任何預測不到的錯誤返回,
不用再麻煩的每次去構造 500 錯誤了。

怎麼樣?快去安利小夥伴們用 echo 寫 HTTP API 吧,真的很方便。

相關文章