🐀 Go | 使用中间件简化 Golang 中的错误处理

在 Golang 的开发中,我们通常需要在代码中所有可能发生的每个地方处理错误,这种做法虽然安全,但是非常麻烦,尤其是当使用 Golang 开发 Web 服务端的时候,需要处理的错误也就更多了。这里我结合了之前一些项目的经验,利用中间件和 Golang 中 panic 和 recover 机制,极大地简化了其错误处理方式,提高了代码的可读性和精简程度。

中间件

首先我们来了解一下在 Web 开发中中间件的形式。

在 Web 开发中,中间件是一种拦截器的思想,根据面向切面编程(AOP)的思想,在某个特定的输入输出之间添加一些额外处理,同时不影响原有操作。这样的做法对原有代码毫无入侵性,把和主业务无关的事情,放到代码外面去做,分离了编码中的关注点。

而不同的路由框架一般都有不同的中间件设计。

一般的中间件都是通过递归调用的形式实现,但是其具体的实现细节有所不同

在比较流行的 Node.js 框架 Express 中,其中间件一般是通过尾递归的形式实现,是一种类似于流水线形式的中间件。请求会一一通过各个中间件,最后到达我们的业务请求中。

而另一个框架 Koa 中,使用的是一个“洋葱圈”的架构,我们的业务逻辑在洋葱中的最核心部分。

165d21b34c3f3768

在这个的项目中,我使用的是 Golang 中 iris 这个框架,其中间件的实现也类似与上面的洋葱圈结构。

定义一个中间件:

1
2
3
4
5
app.Use(func(ctx context.Context) {
// Do something
ctx.Next()
// Do something more
})

Panic & Recover

我们再来看一下 Golang 中的 panic 和 recover 机制。

在 Golang 中,不存在类似于其他语言中try {…} catch {…}的机制,但是我们可以换另一种方式实现。

我们可以通过panic抛出错误,然后在程序的defer函数中使用recover捕获,具体的操作如下:

1
2
3
4
5
6
7
8
9
10
func test() {
defer func () {
if err := recover(); err != nil {
fmt.Println(err)
}
}
fmt.Println("1")
panic("2")
fmt.Println("3")
}

程序的输出为:

1
2
1
2

需要注意的是recover只在defer的函数中有效

错误处理

在 Web 服务端程序中,当发生错误的时候,我们一般都会返回错误的信息,比如权限不足、请求对象不存在还有参数错误等等。一般会这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func Get(ctx context.Context) {
req := ReqStruct{}
err := ctx.ReadJson(&req)
if err != nil {
ctx.SetStatusCode(400)
ctx.JSON(ErrorRes{
Message: 'invalid_req'
})
return
}
user := service.getUser(req.user)
if user.Type != "admin" {
ctx.SetStatusCode(401)
ctx.JSON(ErrorRes{
Message: 'not_admin'
})
return
}
obj, err := service.GetObj(req.ID)
if err != nil {
ctx.SetStatusCode(403)
ctx.JSON(ErrorRes{
Message: 'no_exist'
})
return
}
ctx.JSON(obj)
}

可以看到,上面的代码可读性是非常糟糕的,几乎80%的代码都是错误处理,并且Golang中 if 语法必须使用花括号包围,也就是说每个error的处理至少都要占3行。

这个时候,我们可以使用上面的两种技术,来简化Web服务端中的错误处理。

首先,我们来添加一个错误处理的中间件,在这个中间件中,对于固定格式的异常抛出直接分析里面的数据,封装成HTTP回复直接返回给请求者。如果是未知异常,那么就继续向上层抛出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func NewErrorHandler() context.Handler {
return func(ctx context.Context) {
defer func() {
if err := recover(); err != nil {
if ctx.IsStopped() {
return
}
switch errStr := err.(type) {
case string:
p := strings.Split(errStr, "&")
if len(p) == 3 && p[0] == "knownError" {
statusCode, e := strconv.Atoi(p[1])
if e != nil {
break
}
ctx.StatusCode(statusCode)
b, errJSON := jsoniter.Marshal(ErrorRes{
Message: p[2],
})
if errJSON != nil {
break
}
ctx.ContentType("application/json")
_, err = ctx.Write(b)
if err == nil && statusCode < 500 {
return
}
}
}
panic(err)
}
}()
ctx.Next()
}
}

然后添加两个断言工具函数,专门抛出固定格式的错误,里面包含了错误信息,返回码等数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Assert 条件断言
// 当断言条件为 假 时触发 panic
// 对于当前请求不会再执行接下来的代码,并且返回指定格式的错误信息和错误码
func Assert(condition bool, msg string, code ...int) {
if !condition {
statusCode := 400
if len(code) > 0 {
statusCode = code[0]
}
panic("knownError&" + strconv.Itoa(statusCode) + "&" + msg)
}
}

// AssertErr 错误断言
// 当 error 不为 nil 时触发 panic
// 对于当前请求不会再执行接下来的代码,并且返回指定格式的错误信息和错误码
// 若 msg 为空,则默认为 error 中的内容
func AssertErr(err error, msg string, code ...int) {
if err != nil {
statusCode := 400
if len(code) > 0 {
statusCode = code[0]
}
if msg == "" {
msg = err.Error()
}
panic("knownError&" + strconv.Itoa(statusCode) + "&" + msg)
}
}

然后,上面的业务代码中,我们就可以改成这个样子了:

1
2
3
4
5
6
7
8
9
10
func Get(ctx context.Context) {
req := ReqStruct{}
err := ctx.ReadJson(&req)
AssertErr(err, "invalid_req", 400)
user := service.getUser(req.user)
Assert(user.Type != "admin", "not_admin", 401)
obj, err := service.GetObj(req.ID)
AssertErr(err, "no_exist", 403)
ctx.JSON(obj)
}

不仅代码行数减少了近三分之二,并且代码的可读性也大大提高。

使用这种方法,我们还可以封装出各种便利的函数

比如用户登陆检测,当无法从Session中获取用户ID的时候,应用就会直接抛出错误,返回{message: "invalid_session"}的JSON到请求者,并且不再执行下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 检查登陆状态
func (b *BaseController) checkLogin() primitive.ObjectID {
id := b.Session.GetString("id")
_id, err := primitive.ObjectIDFromHex(id)
utils.AssertErr(err, "invalid_session", 401)
return _id
}

// Post 添加任务
func (c *TaskController) Post() int {
id := c.checkLogin()
// ...
}

小结

使用这种方法,所有请求中的错误都可以使用一句话判断错误并返回错误原因到请求者,而且还能顺便设置不同的返回码,在RESTful API的服务端中,使用地非常愉快,没了一大堆了错误处理代码,打起代码行云流水,看上去也舒服得很,个人感觉极大地提高了开发效率。

在这次项目中,结合 iris 框架的MVC结构,第一次使用这种方法进行开发,感觉效果挺好的。比较吃惊的是,iris 框架的作者居然 star 了我们的项目 (逃

🚀 项目: Github

土豪通道
0%