Gin框架中参数校验优化详解

 更新时间:2023年08月24日 08:37:13   作者:Coder慌  
这篇文章主要为大家详细介绍了Gin框架中参数校验优化的相关知识,文中的示例代码讲解详细,具有一定的学习价值,感兴趣的小伙伴可以了解下

原始方式

gin使用的是 github.com/go-playground/validator 该组件进行入参校验,如下是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-go一个web版 linux(终端[终端回放] 文件 脚本 进程 计划任务)、数据库(mysql postgres)、redis(单机 哨兵 集群)、mongo统一管理操作平台

到此这篇关于Gin框架中参数校验优化详解的文章就介绍到这了,更多相关Gin参数校验内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 并发安全本地化存储go-cache读写锁实现多协程并发访问

    并发安全本地化存储go-cache读写锁实现多协程并发访问

    这篇文章主要介绍了并发安全本地化存储go-cache读写锁实现多协程并发访问,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-10-10
  • Golang Mutex互斥锁源码分析

    Golang Mutex互斥锁源码分析

    本篇文章,我们将一起来探究下Golang Mutex底层是如何实现的,知其然,更要知其所以然。文中的示例代码讲解详细,感兴趣的可以了解一下
    2022-10-10
  • 如何利用Go语言实现LRU Cache

    如何利用Go语言实现LRU Cache

    这篇文章主要介绍了如何利用Go语言实现LRU Cache,LRU是Least Recently Used的缩写,是一种操作系统中常用的页面置换算法,下面我们一起进入文章了解更多内容吧,需要的朋友可以参考一下
    2022-03-03
  • Golang 类型转换的实现(断言、强制、显式类型)

    Golang 类型转换的实现(断言、强制、显式类型)

    将一个值从一种类型转换到另一种类型,便发生了类型转换,在go可以分为断言、强制、显式类型转换,本文就详细的介绍一下这就几种转换方式,具有一定的参考价值,感兴趣的可以了解一下
    2023-09-09
  • Gotify搭建你的消息推送系统

    Gotify搭建你的消息推送系统

    这篇文章主要介绍了Gotify搭建你的消息推送系统,今天要分享的是 gotify,是一个用 go 编写的消息服务端,有需要的朋友可以借鉴参考下,希望能够有所帮助
    2024-01-01
  • go语言环境变量设置全过程

    go语言环境变量设置全过程

    这篇文章主要介绍了go语言环境变量设置全过程,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-05-05
  • PHP结构型模式之组合模式

    PHP结构型模式之组合模式

    这篇文章主要介绍了PHP组合模式Composite Pattern优点与实现,组合模式是一种结构型模式,它允许你将对象组合成树形结构来表示“部分-整体”的层次关系。组合能让客户端以一致的方式处理个别对象和对象组合
    2023-04-04
  • go语言之给定英语文章统计单词数量(go语言小练习)

    go语言之给定英语文章统计单词数量(go语言小练习)

    这篇文章给大家分享go语言小练习给定英语文章统计单词数量,实现思路大概是利用go语言的map类型,以每个单词作为关键字存储数量信息,本文通过实例代码给大家介绍的非常详细,需要的朋友参考下吧
    2020-01-01
  • Go 变长参数函数的实现示例

    Go 变长参数函数的实现示例

    本文主要介绍了Go语言中的变长参数的类型、声明、调用以及其在实际开发中的应用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2026-04-04
  • 十个示例带你深入了解Go语言中的接口

    十个示例带你深入了解Go语言中的接口

    这篇文章主要是通过十个简单的示例带大家深入了解一下Go语言中接口的使用,文中的示例代码简洁易懂,具有一定的学习价值,需要的可以了解一下
    2023-02-02

最新评论