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

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

ECHO 框架

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

Echo Features

這些特性裏,HTTP/2,Auto HTTPS,聽着很熟?這是我以前介紹的 Caddy 也有的特性,
由於 golang 實現這些太容易了。還有 Middleware 裏的一大堆功能也差很少。
咱們在作微服務的時候,這些通用的東西由 API Gateway 統一實現就行了,
若是你寫的是個小的獨立應用的後端,這些開箱即用的功能卻是能提供很大的幫助。github

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

RESTful API 錯誤返回

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

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 去統一處理的方法。json

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, 順着找過去,api

// HTTPErrorHandler is a centralized HTTP error handler.
type HTTPErrorHandler func(error, Context)

它是一個傳入 error 和 Context 而且沒有返回值的函數。
但是知道這些仍是有點暈?並不知道怎麼寫這個函數啊。
不要緊,我這篇文章就是講怎麼寫這個函數的。往下看吧。ruby

定義錯誤結構

因爲 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 ee, ok := err.(*echo.HTTPError); ok {
        code = ee.Code
        key = http.StatusText(code)
        msg = key
    } 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 裏,消費端要是看到這種最壞的狀況,
說明你要被扣獎金了,除非你能夠甩鍋給你依賴的模塊或基礎設施。

第二部分咱們先看看傳進來的錯誤是否是咱們以前定義的,若是是那就太好了。若是不是的話,
有多是 Echo 返回的錯誤,好比路由或者方法沒有找到之類的。若是還不是,
看來是一個其餘的未知錯誤,若是 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 吧,真的很方便。

相關文章
相關標籤/搜索