對(duì) echo 框架進(jìn)行統(tǒng)一的自定義錯(cuò)誤處理

借助移動(dòng)端的增長(zhǎng),如今 RESTful 風(fēng)格的 API 已經(jīng)十分流行贫悄,
用各種語(yǔ)言去寫(xiě)后端 API 都有很成熟方便的方案留美,用 golang 寫(xiě)后端 API 更是生產(chǎn)力的代表,
你可以用不輸 python/ruby 這類(lèi)動(dòng)態(tài)語(yǔ)言的速度镰烧,寫(xiě)出性能高出一兩個(gè)數(shù)量級(jí)的后端 API 。

ECHO 框架

由于 golang 的標(biāo)準(zhǔn)庫(kù)在網(wǎng)絡(luò)方面已經(jīng)很完善贮尉,導(dǎo)致框架發(fā)揮余地不大拌滋。很多高手都說(shuō)朴沿,
用什么框架猜谚,用標(biāo)準(zhǔn)庫(kù)就寫(xiě)好了,框架只是語(yǔ)法糖而已赌渣,還會(huì)限制項(xiàng)目的發(fā)展魏铅。
不過(guò)我們并不是高手,語(yǔ)法糖也是糖坚芜,用一個(gè)趁手的框架還是能提高不少效率的览芳。
要是在半年前,你讓我推薦框架鸿竖,我會(huì)說(shuō)有很多沧竟,都各有優(yōu)缺點(diǎn),除了 beego 隨便選一個(gè)就可以缚忧。
但是來(lái)到2017年悟泵,一個(gè)叫 Echo 的框架脫穎而出。這是我目前最推薦的框架闪水。
Echo 的宣傳語(yǔ)用的是 “高性能糕非,易擴(kuò)展,極簡(jiǎn) Go Web 框架” 球榆。它的一些特性如下圖所示:

Echo Features
Echo Features

這些特性里朽肥,HTTP/2,Auto HTTPS持钉,聽(tīng)著很熟衡招?這是我之前介紹的 Caddy 也有的特性,
因?yàn)?golang 實(shí)現(xiàn)這些太容易了每强。還有 Middleware 里的一大堆功能也差不多蚁吝。
我們?cè)谧鑫⒎?wù)的時(shí)候旱爆,這些通用的東西由 API Gateway 統(tǒng)一實(shí)現(xiàn)就好了,
如果你寫(xiě)的是個(gè)小的獨(dú)立應(yīng)用的后端窘茁,這些開(kāi)箱即用的功能倒是能提供很大的幫助怀伦。

其實(shí)今天我主要想說(shuō)說(shuō)最后一個(gè)特性里提到的,“中心化的 HTTP 錯(cuò)誤處理”山林。

RESTful API 錯(cuò)誤返回

一個(gè)團(tuán)隊(duì)?wèi)?yīng)當(dāng)有一份 RESTful API 的規(guī)范房待,而在規(guī)范中應(yīng)該規(guī)范響應(yīng)格式,包括所有錯(cuò)誤響應(yīng)的格式驼抹。
比如微軟的規(guī)范桑孩,
jsonapi.org 推薦規(guī)范等等。
大部分時(shí)候我們不需要實(shí)現(xiàn)的那么繁瑣框冀,我們規(guī)定一個(gè)簡(jiǎn)單的結(jié)構(gòu):

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

傳統(tǒng)的錯(cuò)誤響應(yīng)可能只有一個(gè)伴隨 HTTP Status code 的 string 類(lèi)型的 message流椒,
如今我們把正常的響應(yīng)格式變成了 JSON ,那么把錯(cuò)誤返回也用 JSON 吧明也。
除了用 JSON 之外宣虾,我們又增加了一個(gè) error 字段,
這個(gè)字段是一個(gè)比 Status code 要詳細(xì)一個(gè)級(jí)別的 Key温数,
消費(fèi)端可以用這個(gè)約定的 Key 做更為靈活的錯(cuò)誤處理绣硝。

好了,我們就用這個(gè)簡(jiǎn)單的例子進(jìn)行下去撑刺,今天主題講的是 Echo 去統(tǒng)一處理的方法鹉胖。

Echo 怎么統(tǒng)一處理錯(cuò)誤?

其實(shí) Echo 的文檔雖然很漂亮够傍,但是不夠詳細(xì)甫菠,深入一點(diǎn)的內(nèi)容和例子并沒(méi)有。
但一個(gè)漂亮的 golang 項(xiàng)目冕屯,代碼即是文檔寂诱,我們應(yīng)該有去 godoc.org 查文檔的習(xí)慣。
我們找到 Echo 的 GoDoc愕撰,
看 Echo 類(lèi)型:

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, 順著找過(guò)去刹衫,

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

它是一個(gè)傳入 error 和 Context 并且沒(méi)有返回值的函數(shù)。
可是知道這些還是有點(diǎn)暈搞挣?并不知道怎么寫(xiě)這個(gè)函數(shù)啊带迟。
沒(méi)關(guān)系,我這篇文章就是講怎么寫(xiě)這個(gè)函數(shù)的囱桨。往下看吧仓犬。

定義錯(cuò)誤結(jié)構(gòu)

由于 golang 是靜態(tài)類(lèi)型,我們干啥都需要先定義個(gè)結(jié)構(gòu)舍肠,代碼如下:

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. 定義了錯(cuò)誤的結(jié)構(gòu)搀继,其中包含 code窘面,key 和 message,key 和 message 可以被導(dǎo)出為 JSON叽躯。
  2. 做了個(gè)新建錯(cuò)誤結(jié)構(gòu)的函數(shù)财边,這樣就可以用一行代碼去新建一個(gè)錯(cuò)誤了。
  3. 給這個(gè)結(jié)構(gòu)增加了 Error 函數(shù)点骑,這樣這個(gè)結(jié)構(gòu)就成了一個(gè) golang 的 error 接口酣难。

處理錯(cuò)誤

我們終于可以寫(xiě)上文提到的自定義函數(shù)了,先看示例代碼我再做解釋黑滴,然后你就能寫(xiě)自己的了:

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

這個(gè)函數(shù)的功能就是根據(jù)傳進(jìn)來(lái)的 error 和上下文 Context憨募,組裝出合適的 HTTP 響應(yīng)。
可因?yàn)?golang 的 error 是一個(gè)接口袁辈,也就是第一個(gè)參數(shù)可能傳進(jìn)來(lái)任何奇怪的東西菜谣,
我們需要細(xì)心的處理一下。

第一部分我們定義了默認(rèn)值作為最壞的情況晚缩,在 HTTP API 里尾膊,消費(fèi)端要是看到這種最壞的情況,
說(shuō)明你要被扣獎(jiǎng)金了橡羞,除非你可以甩鍋給你依賴的模塊或基礎(chǔ)設(shè)施眯停。

第二部分我們先看看傳進(jìn)來(lái)的錯(cuò)誤是不是我們之前定義的济舆,如果是那就太好了卿泽。如果不是的話,
有可能是 Echo 返回的錯(cuò)誤滋觉,比如路由或者方法沒(méi)有找到之類(lèi)的签夭。如果還不是,
看來(lái)是一個(gè)其他的未知錯(cuò)誤椎侠,如果 Debug 開(kāi)著第租,那還好,不用扣獎(jiǎng)金我纪,我們把錯(cuò)誤明細(xì)直接返回
到 msg 里方便調(diào)試慎宾。如果也沒(méi)開(kāi) Debug ... 那只好硬著頭皮返回 500 并什么信息都不給了。

第三部分你可以基本照抄浅悉,是檢查上下文中是否聲明這個(gè)響應(yīng)已經(jīng)提交了趟据,只有沒(méi)提交的時(shí)候,
我們才需要把我們準(zhǔn)備好的錯(cuò)誤信息以 JSON 格式提交术健,順便打印錯(cuò)誤日志汹碱。另外,如果請(qǐng)求
是 HEAD 方法的話荞估,根據(jù)規(guī)范咳促,你只能返回狀態(tài) 204 并默默在日志記錄錯(cuò)誤了稚新。

應(yīng)用

好了,我們寫(xiě)好了統(tǒng)一的錯(cuò)誤處理跪腹,該怎么使用呢褂删? 來(lái)看一個(gè)極簡(jiǎn)的例子吧:

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

這是個(gè)從 mongodb 取 user 的例子,

  1. 檢查url中的id是不是一個(gè)合法的id冲茸,不是的話笤妙,返回我們之前自定義的錯(cuò)誤。
  2. 去數(shù)據(jù)庫(kù)里查噪裕,如果沒(méi)有記錄蹲盘,返回 404 錯(cuò)誤。
  3. 如果查詢數(shù)據(jù)庫(kù)的操作出了其他錯(cuò)誤膳音,這個(gè)時(shí)候我們無(wú)能為力了召衔,只好直接把這個(gè)錯(cuò)誤返回。
  4. 一切正常沒(méi)錯(cuò)誤的話祭陷,我們返回狀態(tài) 200 和 JSON 數(shù)據(jù)苍凛。

我們可以看出,經(jīng)過(guò)這么一番折騰兵志,在寫(xiě)API的時(shí)候醇蝴,省心了很多。
我們可以隨手用一行代碼構(gòu)造錯(cuò)誤想罕,也可以直接把任何預(yù)測(cè)不到的錯(cuò)誤返回悠栓,
不用再麻煩的每次去構(gòu)造 500 錯(cuò)誤了。

怎么樣按价?快去安利小伙伴們用 echo 寫(xiě) HTTP API 吧惭适,真的很方便。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末楼镐,一起剝皮案震驚了整個(gè)濱河市癞志,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌框产,老刑警劉巖凄杯,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異秉宿,居然都是意外死亡戒突,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)蘸鲸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)妖谴,“玉大人,你說(shuō)我怎么就攤上這事∠ゾ耍” “怎么了嗡载?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)仍稀。 經(jīng)常有香客問(wèn)我洼滚,道長(zhǎng),這世上最難降的妖魔是什么技潘? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任遥巴,我火速辦了婚禮,結(jié)果婚禮上享幽,老公的妹妹穿的比我還像新娘铲掐。我一直安慰自己,他們只是感情好值桩,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布摆霉。 她就那樣靜靜地躺著,像睡著了一般奔坟。 火紅的嫁衣襯著肌膚如雪携栋。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,475評(píng)論 1 312
  • 那天咳秉,我揣著相機(jī)與錄音婉支,去河邊找鬼。 笑死澜建,一個(gè)胖子當(dāng)著我的面吹牛向挖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播霎奢,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼户誓,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼饼灿!你這毒婦竟也來(lái)了幕侠?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤碍彭,失蹤者是張志新(化名)和其女友劉穎晤硕,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體庇忌,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡舞箍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了皆疹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片疏橄。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出捎迫,到底是詐尸還是另有隱情晃酒,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布窄绒,位于F島的核電站贝次,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏彰导。R本人自食惡果不足惜蛔翅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望位谋。 院中可真熱鬧山析,春花似錦、人聲如沸掏父。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)损同。三九已至翩腐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間膏燃,已是汗流浹背茂卦。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留组哩,地道東北人等龙。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像伶贰,于是被迫代替她去往敵國(guó)和親蛛砰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容