likes
comments
collection
share

在Gin中使用中间件进行全局错误处理

作者站长头像
站长
· 阅读数 55

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的位置。

在Gin中使用中间件进行全局错误处理 编写错误处理中间件,具体代码如下,把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,并且不会执行后续中间件直接返回到上一级的中间件。

在Gin中使用中间件进行全局错误处理 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进行测试 未登录时访问私有路由,会提示未登录错误,说明中间件之间的错误处理流程正确。 在Gin中使用中间件进行全局错误处理

输入错误邮箱、密码、参数时可以看到能返回对应的错误,说明路由的错误处理流程正确。 在Gin中使用中间件进行全局错误处理

10 总结

Gin提供了全局错误的处理方式,使用中间件来处理错误时最关键的一点是要注意中间件的执行顺序,以及c.Next()和c.Abort()的后续执行逻辑。Gin封装的c.AbortWithError()有点小问题,建议c.Abort()和c.Error()分开使用。

转载自:https://juejin.cn/post/7064770224515448840
评论
请登录