对 echo 框架进行统一的自定义错误处理

借助移动端的增长,如今 RESTful 风格的 API 已经十分流行,
用各种语言去写后端 API 都有很成熟方便的方案,用 golang 写后端 API 更是生产力的代表,
你可以用不输 python/ruby 这类动态语言的速度,写出性能高出一两个数量级的后端 API 。

ECHO 框架

由于 golang 的标准库在网络方面已经很完善,导致框架发挥余地不大。很多高手都说,
用什么框架,用标准库就写好了,框架只是语法糖而已,还会限制项目的发展。
不过我们并不是高手,语法糖也是糖,用一个趁手的框架还是能提高不少效率的。
要是在半年前,你让我推荐框架,我会说有很多,都各有优缺点,除了 beego 随便选一个就可以。
但是来到2017年,一个叫 Echo 的框架脱颖而出。这是我目前最推荐的框架。
Echo 的宣传语用的是 “高性能,易扩展,极简 Go Web 框架” 。它的一些特性如下图所示:

Echo Features
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 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 吧,真的很方便。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容