go gin web应用-通过中间件形式实现通用的参数检验

发布时间 2023-03-25 12:56:37作者: Marathon-Davis

都知道 gin 在web开发方面应用广泛,但在参数校验上,之前写一堆 POST 接口的时候,每个接口的业务代码里都要去实现 validate 校验逻辑,感觉代码复用糟糕。

为解决这问题,想到通过 reflect 包是不是可以实现通用的校验处理呢。如果可以实现,业务逻辑就只需要专注与业务实现,进一步实现高内聚。

main.go

package main

import (
	"errors"
	"fmt"
	"net/http"
	"reflect"
	
	"github.com/gin-gonic/gin"
	"github.com/gin-gonic/gin/binding"
	"github.com/go-playground/validator/v10"
)

func main()  {
	r := gin.New()
	r.Use(PrintRequestURL(), ValidateMiddleware())
	r.POST("/hello", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{
			"msg": fmt.Sprintf("api: %s", ctx.Request.URL),
		})
	})
	r.POST("/ping", func(ctx *gin.Context) {
		ctx.JSON(http.StatusOK, gin.H{
			"msg": fmt.Sprintf("api: %s", ctx.Request.URL),
		})
	})

	_ = r.Run(":8080")
}

// 定义请求处理前后的进出打印中间件
func PrintRequestURL() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		fmt.Printf("Request URL: %s start.\n", ctx.Request.URL)
		ctx.Next()
		fmt.Printf("Request URL: %s end.\n", ctx.Request.URL)
	}
}

// 定义参数自动校验中间件,根据url自动关联对应的结构体进行校验
func ValidateMiddleware() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		// 校验出错,需要及时终止请求下传
		if err := validateParams(ctx); err != nil {
			// 实际项目,不把具体出错传给前台,只需要响应比如 {“msg”: "req params validate error"},后台应该记录具体出错,
			// 细节在后台记录,前台只要收到出错即可
			ctx.JSON(http.StatusOK, gin.H{
				"msg": fmt.Sprintf("params validate error: %s", err),
			})
			ctx.Abort()
		}

		ctx.Next()
	}
}

// 校验函数
func validateParams(ctx *gin.Context) error {
	return validateOrNot(ctx)
}

// 校验函数,通过反射获取原始类型进行 bind和validate
func validateOrNot(ctx *gin.Context) error {
	var err error
	// 定义请求体接收变量
	var params interface{}
	// 获取 url 对应的结构体类型,interface{}
	val, ok := url2struct[ctx.Request.URL.String()]
	if !ok {
		return errors.New("Bad url.")
	}

	// 先从interface{}还原,提取原值类型,再运行时,获取校验体原始类型,
	reqTyp := reflect.Indirect(reflect.ValueOf(val)).Type()
	if reqTyp == nil {
		return errors.New("reqStruct is nil")
	}
	// 原始类型
	params = reflect.New(reqTyp).Interface()
	err = ctx.ShouldBindBodyWith(&params, binding.JSON)
	if err != nil {
		return err
	}
	// 拿到实际请求结构体进行校验
	valid := validator.New()
	err = valid.Struct(params)
	if err != nil {
		return err
	}

	return nil
}

// /hello 对应的请求参数结构体
type HelloParams struct {
	Param1 string `json:"param1" validate:"required,min=5,max=10"`
}

// 实现reqStruct接口方法
func (t HelloParams) do()  {}

// 定义 /ping 的请求结构体
type PingParams struct {
	Param2 string `json:"param2" validate:"required,min=6,max=10"`
}

// 实现reqStruct接口方法
func (t PingParams) do()  {

}

// 初始化 url2struct 变量
func init()  {
	initURL2Struct()
}

type reqStruct interface {
	do()
}

var url2struct map[string]reqStruct

func initURL2Struct()  {
	if url2struct == nil {
		url2struct = make(map[string]reqStruct)
	}
	url2struct = M
}

// 定义 URL: struct{},在新增 接口 需要校验的时候,需要维护此对应关系
var (
	M = map[string]reqStruct{
		"/hello": HelloParams{},
		"/ping": PingParams{},
	}
)

代码注释也比较详细,主要通过两个 POST 接口来测试,以下是 Postman 测试情况:
测试正常情况

测试异常请求参数
参数长度过长

参数长度过短

可以看到,我们通过 reflect + validate 即实现了中间件的形式校验参数,让业务专注业务实现,不同的业务接口通过 map 实现的 url: struct{} 来完成映射,我们只需要每次在新增 POST 接口的时候,添加对应关系即可,扩展性较好,当然反射也会一定程度影响性能,就看取舍了,未做基本测试,具体情况具体分析吧。

参考文章: