go gin web应用-通过中间件形式实现通用的参数检验
都知道 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(¶ms, binding.JSON)
if err != nil {
return err
}
// 拿到实际请求结构体进行校验
valid := validator.New()
err = valid.Struct(params)
if err != nil {
return err
}
// 可以在此注入解析过的请求参数,在handler中就可以直接get到请求参数
ctx.Set(ctx.Request.URL.String(), params)
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
接口的时候,添加对应关系即可,扩展性较好,当然反射也会一定程度影响性能,就看取舍了,未做基本测试,具体情况具体分析吧。
参考文章: