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

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

ECHO 框架

由於 golang 的標準庫在網路方面已經很完善,導致框架發揮餘地不大。很多高手都說, 用什麼框架,用標準庫就寫好了,框架只是語法糖而已,還會限制項目的發展。

不過我們並不是高手,語法糖也是糖,用一個趁手的框架還是能提高不少效率的。

要是在半年前,你讓我推薦框架,我會說有很多,都各有優缺點,除了 beego 隨便選一個就可以。 但是來到2017年,一個叫 Echo 的框架脫穎而出。這是我目前最推薦的框架。

Echo 的宣傳語用的是 「高性能,易擴展,極簡 Go Web 框架」 。它的一些特性如下圖所示:

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

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

RESTful API 錯誤返回

一個團隊應當有一份 RESTful API 的規範,而在規範中應該規範響應格式,包括所有錯誤響應的格式。 比如微軟的規範, jsonapi.org 推薦規範等等。

大部分時候我們不需要實現的那麼繁瑣,我們規定一個簡單的結構:

STATUS 400 Bad Requestn{n "error": "InvalidID",n "message": "invalid id in your url query parameters"n}n

傳統的錯誤響應可能只有一個伴隨 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 {n Server *http.Servern TLSServer *http.Servern Listener net.Listenern TLSListener net.Listenern DisableHTTP2 booln Debug booln HTTPErrorHandler HTTPErrorHandlern Binder Bindern Validator Validatorn Renderer Renderern AutoTLSManager autocert.Managern Mutex sync.RWMutexn Logger Loggern // contains filtered or unexported fieldsn}n

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

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

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

定義錯誤結構

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

type httpError struct {n code intn Key string `json:"error"`n Message string `json:"message"`n}nnfunc newHTTPError(code int, key string, msg string) *httpError {n return &httpError{n code: code,n Key: key,n Message: msg,n }n}nn// Error makes it compatible with `error` interface.nfunc (e *httpError) Error() string {n return e.Key + ": " + e.Messagen}n

這裡我們做了三件事

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

處理錯誤

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

package mainnnimport (n "net/http"nn "github.com/labstack/echo"n)nn// httpErrorHandler customize echos HTTP error handler.nfunc httpErrorHandler(err error, c echo.Context) {n var (n code = http.StatusInternalServerErrorn key = "ServerError"n msg stringn )nn if he, ok := err.(*httpError); ok {n code = he.coden key = he.Keyn msg = he.Messagen } else if config.Debug {n msg = err.Error()n } else {n msg = http.StatusText(code)n }nn if !c.Response().Committed {n if c.Request().Method == echo.HEAD {n err := c.NoContent(code)n if err != nil {n c.Logger().Error(err)n }n } else {n err := c.JSON(code, newHTTPError(code, key, msg))n if err != nil {n c.Logger().Error(err)n }n }n }n}n

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

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

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

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

應用

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

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

這是個從 mongodb 取 user 的例子,

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

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

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


推薦閱讀:

如何讓Scaladoc鏈接到外部API?
再談 API 的撰寫 - 總覽
談談編譯和運行
API 是如何工作的(傻瓜式教學)

TAG:Go语言 | Web开发 | API |