在Gin中使用中间件进行全局错误处理
1 前言
错误处理是Web后台开发中一个非常重要的环节,这次用Gin重构SpringBoot后台时发现Golang的错误处理在Web开发中显得有点啰嗦,在api层(等同于SpringBoot的controller层)不停的对ctx设置错误body未免显得有点繁琐,所以第一时间想到的是进行全局错误处理。但是网上查了一圈博客发现Gin并没有统一的全局错误处理规范,这里记录一下自己觉得一个比较好的做法——中间件处理错误,顺带提一下几个需要注意的点。
2 项目结构
|——gin_err_handler
| |——errors
| |——errors.go
| |——middleware
| |——cookie.go
| |——error.go
| |——model
| |——user.go
| |——service
| |——user.go
| |——go.mod
| |——go.sum
| |——main.go
3 自定义错误结构
首先自定义如下错误结构体,包含code、msg和data,data用以提供附加的错误信息,比如提示300参数错误时data可提示具体的参数错误信息。按照golang的接口实现规则,只要MyError结构体实现了Error() string方法就可被视作实现了标准error接口。
package errors
type MyError struct {
Code int
Msg string
Data interface{}
}
var (
LOGIN_UNKNOWN = NewError(202, "用户不存在")
LOGIN_ERROR = NewError(203, "账号或密码错误")
VALID_ERROR = NewError(300, "参数错误")
ERROR = NewError(400, "操作失败")
UNAUTHORIZED = NewError(401, "您还未登录")
NOT_FOUND = NewError(404, "资源不存在")
INNER_ERROR = NewError(500, "系统发生异常")
)
func (e *MyError) Error() string {
return e.Msg
}
func NewError(code int, msg string) *MyError {
return &MyError{
Msg: msg,
Code: code,
}
}
func GetError(e *MyError, data interface{}) *MyError {
return &MyError{
Msg: e.Msg,
Code: e.Code,
Data: data,
}
}
4 Gin源码中的错误处理建议
查看Gin的源码可知,Context.go的结构体提供了一个Errors字段用以传递错误,并且作者也在Error()方法注释上建议用此方法来存储错误并调用中间件处理错误。
type Context struct {
//......
// Errors is a list of errors attached to all the handlers/middlewares who used this context.
Errors errorMsgs
//......
}
/************************************/
/********* ERROR MANAGEMENT *********/
/************************************/
// Error attaches an error to the current context. The error is pushed to a list of errors.
// It's a good idea to call Error for each error that occurred during the resolution of a request.
// A middleware can be used to collect all the errors and push them to a database together,
// print a log, or append it in the HTTP response.
// Error will panic if err is nil.
func (c *Context) Error(err error) *Error {
if err == nil {
panic("err is nil")
}
parsedError, ok := err.(*Error)
if !ok {
parsedError = &Error{
Err: err,
Type: ErrorTypePrivate,
}
}
c.Errors = append(c.Errors, parsedError)
return parsedError
}
5 使用中间处理错误
知道了Context的错误存储在哪里,那么现在就可以编写中间件来处理错误。需要注意的是Gin中间件的处理流程,如下所示,在中间件调用c.Next()方法后就会进入执行链后面的中间件,执行完后面的中间件后会返回到现在的中间件并执行后续代码。因此我们要想用中间件来完成全局的错误处理,就应该将错误处理中间件放到执行链的最顶端,图中中间件1的位置;并且还要将错误处理的逻辑放到c.Next()后执行,图中代码段2的位置。
编写错误处理中间件,具体代码如下,把c.Next()放在顶端,之后检查c.Errors中是否有错误,如果有自定义错误则将code、msg和data放到response中。若不是自定义错误则提示服务器异常。
package middleware
import (
"gin_err_handler/errors"
"net/http"
"github.com/gin-gonic/gin"
)
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next() // 先调用c.Next()执行后面的中间件
// 所有中间件及router处理完毕后从这里开始执行
// 检查c.Errors中是否有错误
for _, e := range c.Errors {
err := e.Err
// 若是自定义的错误则将code、msg返回
if myErr, ok := err.(*errors.MyError); ok {
c.JSON(http.StatusOK, gin.H{
"code": myErr.Code,
"msg": myErr.Msg,
"data": myErr.Data,
})
} else {
// 若非自定义错误则返回详细错误信息err.Error()
// 比如save session出错时设置的err
c.JSON(http.StatusOK, gin.H{
"code": 500,
"msg": "服务器异常",
"data": err.Error(),
})
}
return // 检查一个错误就行
}
}
}
6 其他中间件设置错误
通常项目中还会包含鉴权的中间件,这里使用的是session来鉴权,当session中间件发现请求没有携带cookie或cookie中没有session信息时,就应该立即返回错误。Gin中提供了c.Abort()来打断中间件的执行链,原理如下图所示,当执行到c.Abort()时中间会立即执行当前的代码段2,并且不会执行后续中间件直接返回到上一级的中间件。
session中间件中的处理逻辑如下所示,检查session中有没有保存当前用户信息(登录成功后会保存),若没有保存则调用c.Abort()打断中间件执行链,并调用c.Error设置自定义错误。
需要注意的是
虽然Gin提供c.AbortWithError()方法整合这两个步骤,但是直接调用有问题,会导致response的Content-type不是JSON,这个Gin自身的问题暂时没有解决办法。
package middleware
import (
"gin_err_handler/errors"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
func Cookie() gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
if session.Get("currentUser") == nil {
c.Abort()
c.Error(errors.UNAUTHORIZED)
// 缩写成c.AbortWithError(errors.UNAUTHORIZED)会有问题
// 导致response的Content-type不是JSON
return
}
c.Next()
}
}
7 路由和Service层中设置错误
service的返回值中包含一个错误字段,当service代码中遇到错误时就返回一个对应的自定义错误。
// service/user.go
package service
import (
"gin_err_handler/errors"
"gin_err_handler/model"
)
type UserService struct{}
var db = model.User{
Id: 1,
Username: "Alice",
Email: "alice@gmail.cn",
Password: "123456",
} // 将用户数据写死在代码里
func (s *UserService) Login(login model.Login) (*model.User, error) {
u := &model.User{}
if login.Email != db.Email {
return nil, errors.LOGIN_UNKNOWN // 202 用户不存在
}
if login.Password != db.Password {
return nil, errors.LOGIN_ERROR
}
*u = db
u.Password = "" // 密码是敏感信息不返回
return u, nil
}
路由里面接收参数并校验,若参数有错则设置错误并返回,因为后续没有handler所以不用Abort。调用service具体业务接口,若出错则设置错误。
// main.go
func setPulicRouter(r *gin.Engine) {
r.POST("/login", func(c *gin.Context) {
var loginVo model.Login
// 产生的一切错误都放到c.Error里
// router里就可以不调用c.JSON()返回错误信息
if e := c.ShouldBindJSON(&loginVo); e != nil {
myErr := errors.VALID_ERROR
myErr.Data = e.Error()
c.Error(myErr)
return
}
u, err := userService.Login(loginVo)
// 若有自定义错误则设置错误
if err != nil {
c.Error(err)
return
}
session := sessions.Default(c)
session.Set("currentUser", u)
if e := session.Save(); e != nil {
// session保存出错也交给中间件处理,非自定义错误
c.Error(e)
return
}
c.JSON(http.StatusOK, gin.H{"code": 200, "msg": "登录成功"})
})
}
8 注册中间件
在main函数中注册中间件时要注意5、6中提到中间件执行顺序,首先将错误处理中间件放到所有路由和其他中间件前面。将公共路由放到session中间件前面,私有路由放到最后注册。
func main() {
gob.Register(model.User{})
r := gin.Default()
r.Use(middleware.ErrorHandler()) // 错误处理中间件放最前面
store := cookie.NewStore([]byte("yoursecret"))
r.Use(sessions.Sessions("GSESSIONID", store))
// 公共路由不需要cookie验证,所以放session中间件前注册
setPulicRouter(r)
r.Use(middleware.Cookie())
setPrivateRouter(r)
r.Run(":8080")
}
9 测试运行
完整项目代码见我的Github示例,输入go run main.go
运行demo程序,这里使用Postman进行测试
未登录时访问私有路由,会提示未登录错误,说明中间件之间的错误处理流程正确。
输入错误邮箱、密码、参数时可以看到能返回对应的错误,说明路由的错误处理流程正确。
10 总结
Gin提供了全局错误的处理方式,使用中间件来处理错误时最关键的一点是要注意中间件的执行顺序,以及c.Next()和c.Abort()的后续执行逻辑。Gin封装的c.AbortWithError()有点小问题,建议c.Abort()和c.Error()分开使用。
转载自:https://juejin.cn/post/7064770224515448840