go-playground/validator。对应的github地址:
https://github.com/go-playground/validator
验证器是基于标签实现结构和单个字段的值验证。它具有以下独特功能:
1,使用验证标签或自定义验证器进行跨字段和跨结构验证。
2,切片、数组和映射潜水,允许验证多维字段的任何或所有级别。
3,能够深入研究地图键和值以进行验证
4,通过在验证之前确定它的基础类型来处理类型接口。
5,处理自定义字段类型,例如 sql driver Valuer 请参阅Valuer
6,别名验证标签,允许将多个验证映射到单个标签,以便更轻松地定义结构上的验证
7,提取自定义字段名称,例如可以指定在验证时提取 JSON 名称,并使其在生成的 FieldError 中可用
8,可定制的 i18n 感知错误消息。
安装此验证器只需要下面几个步骤,第一步在项目模块获取包,第二步将验证器引入到代码中:
go get github.com/go-playground/validator/v10
import "github.com/go-playground/validator/v10"
返回校验错误值,Validator 仅返回错误验证输入的 InvalidValidationError、nil 或 ValidationErrors 作为类型错误;因此,在您的代码中,您需要做的就是检查返回的错误是否不为零,如果不是,则检查错误是否为 InvalidValidationError (如果需要,大多数情况下不是)类型将其转换为类型 ValidationErrors 像这样:
err := validate.Struct(mystruct)
validationErrors := err.(validator.ValidationErrors)
这部分我们通过一个简单的例子来入门学习一下数据的校验。
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
// User contains user information
type User struct {
FirstName string `validate:"required"` //不能为空
LastName string `validate:"required"` //不能为空
Age uint8 `validate:"gte=0,lte=130"` //值大于等于0 小于等于100
Email string `validate:"required,email"` //不能为空 满足邮箱类型
FavouriteColor string `validate:"iscolor"` //是颜色数据 // alias for 'hexcolor|rgb|rgba|hsl|hsla'
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}
// Address houses a users address information
type Address struct {
Street string `validate:"required"` //不能为空
City string `validate:"required"` //不能为空
Planet string `validate:"required"` //不能为空
Phone string `validate:"required"` //不能为空
}
// use a single instance of Validate, it caches struct info
var validate *validator.Validate
func main() {
validate = validator.New()
validateStruct()
validateVariable()
}
func validateStruct() {
address := &Address{
Street: "Eavesdown Docks",
Planet: "Persphone",
Phone: "none",
}
user := &User{
FirstName: "Badger",
LastName: "Smith",
Age: 135,
Email: "Badger.Smith@gmail.com",
FavouriteColor: "#000-",
Addresses: []*Address{address},
}
// returns nil or ValidationErrors ( []FieldError )
err := validate.Struct(user)
if err != nil {
// this check is only needed when your code could produce
// an invalid value for validation such as interface with nil
// value most including myself do not usually have code like this.
if _, ok := err.(*validator.InvalidValidationError); ok {
fmt.Println(err)
return
}
for _, err := range err.(validator.ValidationErrors) {
fmt.Println("命名空间:", err.Namespace())
fmt.Println("字段:", err.Field())
fmt.Println("结构命名空间:", err.StructNamespace())
fmt.Println("结构字段:", err.StructField())
fmt.Println("标签:", err.Tag())
fmt.Println("实际标签:", err.ActualTag())
fmt.Println("字段种类:", err.Kind())
fmt.Println("字段类型:", err.Type())
fmt.Println("字段值:", err.Value())
fmt.Println("字段校验值:", err.Param())
fmt.Printf("错误提示信息:%v\n", err)
fmt.Println()
}
// from here you can create your own error messages in whatever language you wish
return
}
// save user to database
}
func validateVariable() {
myEmail := "joeybloggs.gmail.com"
errs := validate.Var(myEmail, "required,email")
if errs != nil {
fmt.Println(errs) // output: Key: "" Error:Field validation for "" failed on the "email" tag
return
}
// email ok, move on
}
运行validateStruct方法,可以看到控制台输出下面错误信息:
命名空间: User.Age
字段: Age
结构命名空间: User.Age
结构字段: Age
标签: lte
实际标签: lte
字段种类: uint8
字段类型: uint8
字段值: 135
字段校验值: 130
错误提示信息:Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag
命名空间: User.FavouriteColor
字段: FavouriteColor
结构命名空间: User.FavouriteColor
结构字段: FavouriteColor
标签: iscolor
实际标签: hexcolor|rgb|rgba|hsl|hsla
字段种类: string
字段类型: string
字段值: #000-
字段校验值:
错误提示信息:Key: 'User.FavouriteColor' Error:Field validation for 'FavouriteColor' failed on the 'iscolor' tag
命名空间: User.Addresses[0].City
字段: City
结构命名空间: User.Addresses[0].City
结构字段: City
标签: required
实际标签: required
字段种类: string
字段类型: string
字段值:
字段校验值:
错误提示信息:Key: 'User.Addresses[0].City' Error:Field validation for 'City' failed on the 'required' tag
可以看出触发了三条规则,Age不在指定范围,City字段为空,FavouriteColor字段不属于颜色范围。
运行validateVariable方法,可以看到控制台输出下面错误信息:
Key: '' Error:Field validation for '' failed on the 'email' tag
这是因为输出参数不满足email校验的格式。
本部分学一下自定义字段类型的校验,编写如下代码:
package main
import (
"database/sql"
"database/sql/driver"
"fmt"
"reflect"
"time"
"github.com/go-playground/validator/v10"
)
type NullTime struct {
Time time.Time
Valid bool // Valid is true if String is not NULL
}
func (n NullTime) Value() (driver.Value, error) {
if !n.Valid {
return nil, nil
}
return n.Time, nil
}
// DbBackedUser User struct
type DbBackedUser struct {
Name sql.NullString `validate:"required"`
Age sql.NullInt64 `validate:"required"`
Time NullTime `validate:"required,gte='2022-09-09 09:09:09'"`
}
// use a single instance of Validate, it caches struct info
var validate *validator.Validate
func main() {
validate = validator.New()
// register all sql.Null* types to use the ValidateValuer CustomTypeFunc
validate.RegisterCustomTypeFunc(ValidateValuer, sql.NullString{}, sql.NullInt64{}, sql.NullBool{}, sql.NullFloat64{}, NullTime{})
// build object for validation
x := DbBackedUser{Name: sql.NullString{String: "100", Valid: true}, Age: sql.NullInt64{Int64: 10, Valid: true}, Time: NullTime{Time: time.Now(), Valid: true}}
err := validate.Struct(x)
if err != nil {
fmt.Printf("Err(s):\n%+v\n", err)
}
}
// ValidateValuer implements validator.CustomTypeFunc
func ValidateValuer(field reflect.Value) interface{} {
if valuer, ok := field.Interface().(driver.Valuer); ok {
val, err := valuer.Value()
if err == nil {
return val
}
// handle the error how you want
}
return nil
}
本例子主要校验sql.NullString、sql.NullInt64 、NullTime这三种字段类型,其中NullTime是我们自定义的。运行上面代码,可以在控制台看到下面输出结果:
Key: 'DbBackedUser.Time' Error:Field validation for 'Time' failed on the 'gte' tag
如果我们修改参数,将数据修改成下面:
x := DbBackedUser{Name: sql.NullString{String: "", Valid: true}, Age: sql.NullInt64{Int64: 0, Valid: true}, Time: NullTime{Valid: true}}
运行可以看到控制台输出下面三条规则:
Key: 'DbBackedUser.Name' Error:Field validation for 'Name' failed on the 'required' tag
Key: 'DbBackedUser.Age' Error:Field validation for 'Age' failed on the 'required' tag
Key: 'DbBackedUser.Time' Error:Field validation for 'Time' failed on the 'required' tag
这部分通过实现一个结构层的规则校验,编写下面代码:
package main
import (
"fmt"
"reflect"
"strings"
"github.com/go-playground/validator/v10"
)
/**
* 定义一个User的结构体
* FirstName 和 LastName 这两个字段在结构体中没有定义校验规则
*/
type User struct {
FirstName string `json:"fname"`
LastName string `json:"lname"`
Age uint8 `validate:"gte=0,lte=130"`
Email string `json:"e-mail" validate:"required,email"`
FavouriteColor string `validate:"hexcolor|rgb|rgba"`
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}
/**
* 定义一个Address的结构体
*/
type Address struct {
Street string `validate:"required"`
City string `validate:"required"`
Planet string `validate:"required"`
Phone string `validate:"required"`
}
/**
* 定义一个validate这样赋值以后可以全局使用
*/
var validate *validator.Validate
func main() {
validate = validator.New()
/**
* register function to get tag name from json tags.
* 获取tag标签中json标明的字符串
*/
validate.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
// register validation for 'User' 为User注册校验器
// NOTE: only have to register a non-pointer type for 'User', validator 注册的User是一个非指针
// internally dereferences during it's type checks. 在类型检查期间进行内部引用。
validate.RegisterStructValidation(UserStructLevelValidation, User{})
// 赋值
address := &Address{
Street: "Eavesdown Docks",
Planet: "Persphone",
Phone: "none",
City: "Unknown",
}
user := &User{
FirstName: "zhangsan",
LastName: "",
Age: 45,
Email: "Badger.Smith@gmail",
FavouriteColor: "#000",
Addresses: []*Address{address},
}
// returns InvalidValidationError for bad validation input, nil or ValidationErrors ( []FieldError )
// 返回校验不通过的信息
err := validate.Struct(user)
if err != nil {
// this check is only needed when your code could produce
// an invalid value for validation such as interface with nil
// value most including myself do not usually have code like this.
if _, ok := err.(*validator.InvalidValidationError); ok {
fmt.Println(err)
return
}
for _, err := range err.(validator.ValidationErrors) {
fmt.Println(err.Namespace()) // can differ when a custom TagNameFunc is registered or
fmt.Println(err.Field()) // by passing alt name to ReportError like below
fmt.Println(err.StructNamespace())
fmt.Println(err.StructField())
fmt.Println(err.Tag())
fmt.Println(err.ActualTag())
fmt.Println(err.Kind())
fmt.Println(err.Type())
fmt.Println(err.Value())
fmt.Println(err.Param())
fmt.Printf("错误信息:%v\n", err)
fmt.Println()
}
// from here you can create your own error messages in whatever language you wish
return
}
// save user to database
}
// UserStructLevelValidation contains custom struct level validations that don't always
// make sense at the field validation level. For Example this function validates that either
// FirstName or LastName exist; could have done that with a custom field validation but then
// would have had to add it to both fields duplicating the logic + overhead, this way it's
// only validated once.
//UserStructLevelValidation包含自定义结构级验证,这些验证并不总是有效 在现场验证级别有意义。
// 例如,此函数验证 名或姓存在;可以通过自定义字段验证完成,但是 必须将其添加到两个字段中,复制逻辑+开销,这样
//仅验证一次。
// NOTE: you may ask why wouldn't I just do this outside of validator, because doing this way
// hooks right into validator and you can combine with validation tags and still have a
// common error output format.
//注意:您可能会问,为什么我不在验证器外部执行此操作,因为这样做会直接连接到验证器中,您可以与验证标记组合,并且仍然有一个
//输出格式的常见错误。
func UserStructLevelValidation(sl validator.StructLevel) {
user := sl.Current().Interface().(User)
//这里自定义了一种组合校验规则,只有当FirstName和LastName都为空的时候才会触发
if len(user.FirstName) == 0 && len(user.LastName) == 0 {
sl.ReportError(user.FirstName, "fname", "FirstName", "fnameorlname", "")
sl.ReportError(user.LastName, "lname", "LastName", "fnameorlname", "")
}
// 这里可以做一些其他的事情
}
在这个例子中,我们定义了一个结构体User,它里面的FirstName和LastName字段后面并没有加tag标签进行校验,而是通过UserStructLevelValidation这个校验,我们可以在这个里面灵活写一些校验规则,并把规则注册到此结构体。
validate.RegisterStructValidation(UserStructLevelValidation, User{})
运行上面代码,发现只有当FirstName和LastName两个字段都为空的时候才会触发单独注册到结构体里面的规则。
通过前面的几个例子发现,触发校验规则之后输出的原因是一个很长的字符串,看起来不是很直观,这里我们可以映射和自定义错误,编写下面代码:
package main
import (
"fmt"
"github.com/go-playground/locales/en"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
)
// User contains user information
type User struct {
FirstName string `validate:"required"`
LastName string `validate:"required"`
Age uint8 `validate:"gte=0,lte=130"`
Email string `validate:"required,email"`
FavouriteColor string `validate:"iscolor"` // alias for 'hexcolor|rgb|rgba|hsl|hsla'
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}
// Address houses a users address information
type Address struct {
Street string `validate:"required"`
City string `validate:"required"`
Planet string `validate:"required"`
Phone string `validate:"required"`
}
// use a single instance , it caches struct info
var (
uni *ut.UniversalTranslator
validate *validator.Validate
)
func main() {
// NOTE: ommitting allot of error checking for brevity
en := en.New()
uni = ut.New(en, en)
// this is usually know or extracted from http 'Accept-Language' header
// also see uni.FindTranslator(...)
trans, _ := uni.GetTranslator("en")
validate = validator.New()
en_translations.RegisterDefaultTranslations(validate, trans)
translateAll(trans)
translateIndividual(trans)
translateOverride(trans) // yep you can specify your own in whatever locale you want!
}
func translateAll(trans ut.Translator) {
type User struct {
Username string `validate:"required"`
Tagline string `validate:"required,lt=10"`
Tagline2 string `validate:"required,gt=1"`
}
user := User{
Username: "",
Tagline: "This tagline is way too long.",
Tagline2: "1",
}
err := validate.Struct(user)
if err != nil {
// translate all error at once
errs := err.(validator.ValidationErrors)
// returns a map with key = namespace & value = translated error
// NOTICE: 2 errors are returned and you'll see something surprising
// translations are i18n aware!!!!
// eg. '10 characters' vs '1 character'
fmt.Println(errs.Translate(trans))
}
}
func translateIndividual(trans ut.Translator) {
type User struct {
Username string `validate:"required"`
}
var user User
err := validate.Struct(user)
if err != nil {
errs := err.(validator.ValidationErrors)
for _, e := range errs {
// can translate each error one at a time.
fmt.Println(e.Translate(trans))
}
}
}
/**
* 重写required输出
*/
func translateOverride(trans ut.Translator) {
validate.RegisterTranslation("required", trans, func(ut ut.Translator) error {
return ut.Add("required", "{0} must have a value!", true) // see universal-translator for details
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("required", fe.Field())
return t
})
type User struct {
Username string `validate:"required"`
}
var user User
err := validate.Struct(user)
if err != nil {
errs := err.(validator.ValidationErrors)
for _, e := range errs {
// can translate each error one at a time.
fmt.Println(e.Translate(trans))
}
}
}
运行上面代码,可以在控制台看到,前两个案例是按照翻译后输出的,第三个测试案例的规则是按照自定义规则输出来的:
map[User.Tagline:Tagline must be less than 10 characters in length User.Tagline2:Tagline2 must be greater than 1 character in length User.Username:Username is a required field]
Username is a required field
Username must have a value!
通过上部分的学习,我们已经可以翻译和自定义错误,如果觉得英文看起来还是不直观,可以国际化成中文,编写下面代码:
package main
import (
"fmt"
"net/http"
"reflect"
"strings"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/locales/en"
"github.com/go-playground/locales/zh"
ut "github.com/go-playground/universal-translator"
"github.com/go-playground/validator/v10"
en_translations "github.com/go-playground/validator/v10/translations/en"
zh_translations "github.com/go-playground/validator/v10/translations/zh"
)
var trans ut.Translator
type LoginForm struct {
User string `json:"user" binding:"required,min=3,max=10"`
Password string `json:"password" binding:"required"`
}
type SignUpForm struct {
Age uint8 `json:"age" binding:"gte=1,lte=130"`
Name string `json:"name" binding:"required,min=3"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required"`
RePassword string `json:"re_password" binding:"required,eqfield=Password"` //跨字段
}
func removeTopStruct(fileds map[string]string) map[string]string {
rsp := map[string]string{}
for field, err := range fileds {
rsp[field[strings.Index(field, ".")+1:]] = err
}
return rsp
}
func InitTrans(locale string) (err error) {
//修改gin框架中的validator引擎属性, 实现定制
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
//注册一个获取json的tag的自定义方法
v.RegisterTagNameFunc(func(fld reflect.StructField) string {
name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
zhT := zh.New() //中文翻译器
enT := en.New() //英文翻译器
//第一个参数是备用的语言环境,后面的参数是应该支持的语言环境
uni := ut.New(enT, zhT, enT)
trans, ok = uni.GetTranslator(locale)
if !ok {
return fmt.Errorf("uni.GetTranslator(%s)", locale)
}
switch locale {
case "en":
en_translations.RegisterDefaultTranslations(v, trans)
case "zh":
zh_translations.RegisterDefaultTranslations(v, trans)
default:
en_translations.RegisterDefaultTranslations(v, trans)
}
return
}
return
}
func main() {
//代码侵入性很强 中间件
if err := InitTrans("zh"); err != nil {
fmt.Println("初始化翻译器错误")
return
}
router := gin.Default()
router.POST("/loginJSON", func(c *gin.Context) {
var loginForm LoginForm
if err := c.ShouldBind(&loginForm); err != nil {
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
"error": removeTopStruct(errs.Translate(trans)),
})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "登录成功",
})
})
router.POST("/signup", func(c *gin.Context) {
var signUpFrom SignUpForm
if err := c.ShouldBind(&signUpFrom); err != nil {
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"msg": err.Error(),
})
return
}
c.JSON(http.StatusBadRequest, gin.H{
"error": removeTopStruct(errs.Translate(trans)),
})
return
}
c.JSON(http.StatusOK, gin.H{
"msg": "注册成功",
})
})
_ = router.Run(":8080")
}
在上面的代码中,我们加上了中文部分。启动上面实例,分别调用两个接口,可以看到异常原因已经变成中文了:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U489KRgL-1660124878923)(D:\developsoftware\mayun\note\study-note\go\images\image-20220810174338790.png)]](https://1000bd.com/contentImg/2022/08/14/121401626.png)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ddujJ0PZ-1660124878933)(D:\developsoftware\mayun\note\study-note\go\images\image-20220810174517215.png)]](https://1000bd.com/contentImg/2022/08/14/121401810.png)