Gin-参数校验优化
原始方式
gin使用的是 github.com/go-playgrou… 该组件进行入参校验,如下是gin中常用的参数校验方式:
type AccountCreateForm struct {
Id uint64 `json:"id"`
Name string `json:"name" binding:"required,max=16"` // 使用required和max限制入参为必填项与最大长度不能超过16字符
Username string `json:"username" binding:"required"`
Password string `json:"password"`
}
该种方式有如下几个不好使的地方:
- 错误提示不友好,如果不做任何处理,默认参数校验不通过会返回如下错误提示
- 不支持正则表达式
- 不支持自定义错误描述
改进
自定义validatorx(validator扩展工具包)
注册翻译器,新增对校验错误进行转译方法
package validatorx
import (
"mayfly-go/pkg/utils/stringx"
"mayfly-go/pkg/utils/structx"
"reflect"
"strings"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
zh_trans "github.com/go-playground/validator/v10/translations/zh"
)
const CustomMsgTagName = "msg"
var (
trans ut.Translator
)
func Init() {
// 获取gin的校验器
validate, ok := binding.Validator.Engine().(*validator.Validate)
if !ok {
return
}
// 修改返回字段key的格式
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
// 如果存在校验错误提示消息,则使用字段名,后续需要通过该字段名获取相应错误消息
if _, ok := fld.Tag.Lookup(CustomMsgTagName); ok {
return fld.Name
}
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
// 注册翻译器
zh := zh.New()
uni := ut.New(zh, zh)
trans, _ = uni.GetTranslator("zh")
// 注册翻译器
zh_trans.RegisterDefaultTranslations(validate, trans)
// 注册自定义正则表达式校验器
validate.RegisterValidation(CustomPatternTagName, patternValidFunc)
// 注册自定义正则校验规则
RegisterCustomPatterns()
}
// Translate 翻译错误信息
func Translate(data any, err error) map[string][]string {
var result = make(map[string][]string)
errors := err.(validator.ValidationErrors)
for _, err := range errors {
fieldName := err.Field()
// 判断该字段是否设置了自定义的错误描述信息,存在则使用自定义错误信息进行提示
if field, ok := structx.IndirectType(reflect.TypeOf(data)).FieldByName(fieldName); ok {
if errMsg, ok := field.Tag.Lookup(CustomMsgTagName); ok {
customMsg := getCustomErrMsg(err.Tag(), errMsg)
if customMsg != "" {
result[fieldName] = append(result[fieldName], customMsg)
continue
}
}
}
// 如果是自定义正则校验规则,则使用自定义的错误描述信息
if err.Tag() == CustomPatternTagName {
result[fieldName] = append(result[fieldName], fieldName+patternErrMsg[err.Param()])
continue
}
result[fieldName] = append(result[fieldName], err.Translate(trans))
}
return result
}
// 获取自定义的错误提示消息
//
// @param validTag 校验标签,如required等
// @param customMsg 自定义错误消息
func getCustomErrMsg(validTag, customMsg string) string {
// 解析 msg:"required=用户名不能为空,min=用户名长度不能小于8位"
msgs := strings.Split(customMsg, ",")
for _, msg := range msgs {
tagAndMsg := strings.Split(stringx.Trim(msg), "=")
if len(tagAndMsg) > 1 && validTag == stringx.Trim(tagAndMsg[0]) {
// 获取valid tag对应的错误消息
return stringx.Trim(tagAndMsg[1])
}
}
return customMsg
}
// Translate 翻译错误信息为字符串
func Translate2Str(data any, err error) string {
res := Translate(data, err)
errMsgs := make([]string, 0)
for _, v := range res {
errMsgs = append(errMsgs, v...)
}
return strings.Join(errMsgs, ", ")
}
自定义正则表达式校验方式
package validatorx
import (
"mayfly-go/pkg/global"
"regexp"
"github.com/go-playground/validator/v10"
)
const CustomPatternTagName = "pattern"
var (
regexpMap map[string]*regexp.Regexp // key:正则表达式名称 value:正则表达式
patternErrMsg map[string]string // key:正则表达式名称 value:校验不通过时的错误消息提示
)
// 注册自定义正则表达式校验规则
func RegisterCustomPatterns() {
// 账号用户名校验,使用该种方式可以复用正则表达式以及错误提示
// 使用方式如:Username string `json:"username" binding:"pattern=account_username"`
RegisterPattern("account_username", "^[a-zA-Z0-9_]{5,20}$", "只允许输入5-20位大小写字母、数字、下划线")
}
// 注册自定义正则表达式
func RegisterPattern(patternName string, regexpStr string, errMsg string) {
if regexpMap == nil {
regexpMap = make(map[string]*regexp.Regexp, 0)
patternErrMsg = make(map[string]string)
}
regexpMap[patternName] = regexp.MustCompile(regexpStr)
patternErrMsg[patternName] = errMsg
}
// 自定义正则表达式校验器函数
func patternValidFunc(f validator.FieldLevel) bool {
reg := regexpMap[f.Param()]
if reg == nil {
global.Log.Warnf("%s的正则校验规则不存在!", f.Param())
return false
}
return reg.MatchString(f.Field().String())
}
错误转译
对入参进行校验,检验不通过时将错误进行转译,转译为汉字或自定义的错误描述等。
// 绑定并校验请求结构体参数
func BindJsonAndValid[T any](g *gin.Context, data T) T {
if err := g.ShouldBindJSON(data); err != nil {
// 统一recover处理
panic(ConvBindValidationError(data, err))
} else {
return data
}
}
// 绑定请求体中的json至form结构体,并拷贝至另一结构体
func BindJsonAndCopyTo[T any](g *gin.Context, form any, toStruct T) T {
BindJsonAndValid(g, form)
structx.Copy(toStruct, form)
return toStruct
}
// 转译参数校验错误,并将参数校验错误为业务异常错误(统一recover处理)
func ConvBindValidationError(data any, err error) error {
if e, ok := err.(validator.ValidationErrors); ok {
// 调用validatorx.Translate2Str方法进行校验错误转译
return biz.NewBizErrCode(403, validatorx.Translate2Str(data, e))
}
return err
}
// 返回失败结果集
func ErrorRes(g *gin.Context, err any) {
switch t := err.(type) {
case biz.BizError:
g.JSON(http.StatusOK, model.Error(t))
case error:
g.JSON(http.StatusOK, model.ServerError())
global.Log.Errorf("%s\n%s", t.Error(), string(debug.Stack()))
case string:
g.JSON(http.StatusOK, model.ServerError())
global.Log.Errorf("%s\n%s", t, string(debug.Stack()))
default:
global.Log.Error(t)
}
}
初始化校验器
项目启动时,在合适的时机初始化校验器
// 参数校验器初始化、如错误提示中文转译、注册自定义校验器等
validatorx.Init()
统一使用方式
入参字段tag绑定
type AccountCreateForm struct {
Id uint64 `json:"id"`
// msg tag里对应的required max即为binding里的校验类型
Name string `json:"name" binding:"required,max=16" msg:"required=姓名不能为空,max=姓名最大长度不能超过16位"`
// account_name为validatorx.RegisterPattern("account_username", "^[a-zA-Z0-9_]{5,20}$", "只允许输入5-20位大小写字母、数字、下划线")
Username string `json:"username" binding:"pattern=account_username"`
Password string `json:"password" binding:"required"`
}
form := &form.AccountCreateForm{}
// 校验不通过会自行panic统一recover处理
var account *entity.Account = ginx.BindJsonAndCopyTo(rc.GinCtx, form, new(entity.Account))
效果
更多代码详见:gitee.com/objs/mayfly… 一个web版 linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres)、redis(单机 哨兵 集群)、mongo统一管理操作平台
转载自:https://juejin.cn/post/7270160291412656180