Gin的参数校验器validator

安装

1
go get github.com/go-playground/validator/v10

使用

gin自带参数检查机制,通过validator库实现参数检查,通过打tag binding:""实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)

type User struct {
Name string `binding:"required"`
Age string `binding:"required"`
}

func main() {
e := gin.Default()
e.Handle("POST", "/user", func(c *gin.Context) {
u := User{}
err := c.BindJSON(&u)
if err != nil {
log.Println(err)
c.JSON(http.StatusBadRequest, gin.H{
"msg": err.Error(),
})
return
}
log.Printf("user: %+v", u)
})

err := http.ListenAndServe(":8080", e)
if err != nil {
log.Println(err)
return
}
}

在检查结构体参数的时候,也可以直接通过validator来检查参数是否合理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type User struct {
Name string `validate:"required"`
Age string `validate:"required"` // tag是validate
}

func main() {
u1 := User{
Name: "mitaka",
}
v := validator.New()
if err := v.Struct(u1); err != nil {
log.Fatalln(err)
}
}

或者有单独验证的需求,可以设置一个特殊的验证器,例如验证电话号码是否是一个正常的电话号码: example code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
type User struct {
Name string `json:"name"`
// binding的名称是mobile,与后续注册是保持一致
Mobile string `json:"mobile" binding:"required,mobile"`
}

func ValidateMobile(fl validator.FieldLevel) bool {
mobile := fl.Field().String()
// 正则表达式
ok, _ := regexp.MatchString(`^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$`, mobile)
if ok {
return true
}
return false
}

func main() {
r := gin.Default()

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册
v.RegisterValidation("mobile", ValidateMobile)
}

r.POST("", Login)
r.Run(":8080")
}

func Login(c *gin.Context) {
var u User
err := c.ShouldBind(&u) // 当密码为空,返回200,返回值为空
if err != nil {
c.JSON(http.StatusOK, err.Error())
return
}

c.JSON(http.StatusOK, u)
return
}

常用的参数检查

字符串约束

1
2
3
4
5
6
7
8
9
excludesall:不包含参数中任意的 UNICODE 字符
Name string `validate:"excludesall=abc"` // 不能包含a或者b或者c
excludesrune:不包含参数表示的 rune 字符
startswith:以参数子串为前缀
endswith:以参数子串为后缀
contains=:包含参数子串
containsany:包含参数中任意的 UNICODE 字符
containsrune:包含参数表示的 rune 字符
excludes:不包含参数子串 // 不能包含abc

范围约束

  • 对数值,可以约束值
  • 对切片、数组、map,可以约束长度
  • 对string,可以约束长度
1
2
3
4
5
6
7
8
9
10
ne:不等于参数值,一般作用于数值
gt:大于参数值,一般作用于数值
gte:大于等于参数值,一般作用于数值
lt:小于参数值,一般作用于数值
lte:小于等于参数值,一般作用于数值
oneof:列举出的值只能取一个,oneof=xu mitaka,一般作用于数值或者string
eq:等于参数值,
len:字符串长度等于参数值,或者约束数值等于这个值
max:小于等于参数值
min:大于等于参数值

Fields约束

1
2
eqfield:定义字段间的相等约束,用于约束同一结构体中的字段。前提是两个字段类型一致。
nefield:用来约束两个字段不相同

常用约束

1
2
3
4
5
6
7
8
9
unique:指定唯一性约束,不同类型处理不同: validate:"unique"
对于map,unique约束没有重复的值
对于数组和切片,unique没有重复的值
对于元素类型为结构体的碎片,unique约束结构体对象的某个字段不重复,使用unique=field指定字段名
email:使用email来限制字段必须是邮件形式
omitempty:字段未设置,则忽略
-:跳过该字段,不检验;
|:使用多个约束,只需要满足其中一个,例如rgb|rgba;
required:字段必须设置,不能为默认值;

更多的约束方式,可以在官网中查看:validator

自定义结构体校验

例如创建约束方式

1
2
3
4
5
6
7
8
9
func timing(fl validator.FieldLevel) bool {
if date, ok := fl.Field().Interface().(time.Time); ok {
today := time.Now()
if today.Before(date) {
return false // 判断传入时间不能大于当前时间
}
}
return true
}

注册约束

1
err := v.RegisterValidation("timing", timing)

字段约束

1
Date     time.Time `validate:"timing,required"`

国际化

在弹出的报错中,响应如下

1
Key: 'User.Name' Error:Field validation for 'Name' failed on the 'min' tag

一眼不好看出来,validate包自带翻译功能,可以将报错翻译成不同的语言。

例如中文

1
2
3
4
5
6
7
8
9
10
11
zhT := zh.New() //chinese
uni := ut.New(zhT, zhT)

trans, found := uni.GetTranslator("zh")
if !found {
log.Fatalln("zh not found")
}
err = chTranslations.RegisterDefaultTranslations(v, trans)
if err != nil {
log.Fatalln(err)
}

报错断言

1
2
3
4
5
6
errs, ok := err.(validator.ValidationErrors)
if !ok {
log.Fatalln("转义错误")
}
//validator.ValidationErrors类型错误则进行翻译
log.Fatalln(errs.Translate(trans))

在实际的代码中,可以通过header中传入的语言类型进行判断,注册不同的校验语言

支持的语言在 github.com/go-playground/validator/v10/translations/中可以看到。

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package main

import (
"log"
"net/http"
"regexp"

"github.com/gin-gonic/gin"
"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"
chTranslations "github.com/go-playground/validator/v10/translations/zh"
)

type User struct {
Name string `json:"name"`
Mobile string `json:"mobile" binding:"required,mobile"`
}

func ValidateMobile(fl validator.FieldLevel) bool {
mobile := fl.Field().String()
ok, _ := regexp.MatchString(`^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$`, mobile)
if ok {
return true
}
return false
}

var trans ut.Translator

func main() {
zhT := zh.New() //chinese
uni := ut.New(zhT, zhT)

var found bool
trans, found = uni.GetTranslator("zh")
if !found {
log.Fatalln("zh not found")
}

r := gin.Default()

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// 注册全局翻译
_ = chTranslations.RegisterDefaultTranslations(v, trans)
// 注册参数校验器
_ = v.RegisterValidation("mobile", ValidateMobile)
// 注册新的翻译
_ = v.RegisterTranslation("mobile", trans, func(ut ut.Translator) error {
return ut.Add("mobile", "{0} 非法的手机号", false)
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("mobile", fe.Field())
return t
})
}

r.POST("", Login)
r.Run(":8080")
}

func Login(c *gin.Context) {
var u User
err := c.ShouldBind(&u) // 当密码为空,返回200,返回值为空
if err != nil {
ValidatorError(c, err)
return
}

c.JSON(http.StatusOK, u)
return
}

func ValidatorError(c *gin.Context, err error) {
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"message": err.Error(),
})
}
// 将错误信息返回给前端
c.JSON(http.StatusOK, gin.H{
"error": errs.Translate(trans),
})
return
}

此时报错如下

1
2
3
4
5
{
"error": {
"User.Mobile": "Mobile为必填字段"
}
}

暴露出结构体内部信息,因此,还需要再修饰一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package main

import (
"log"
"net/http"
"reflect"
"regexp"
"strings"

"github.com/gin-gonic/gin"
"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"
chTranslations "github.com/go-playground/validator/v10/translations/zh"
)

type User struct {
Name string `json:"name"`
Mobile string `json:"mobile" binding:"required,mobile"`
}

func ValidateMobile(fl validator.FieldLevel) bool {
mobile := fl.Field().String()
ok, _ := regexp.MatchString(`^1(3[0-9]|4[01456879]|5[0-35-9]|6[2567]|7[0-8]|8[0-9]|9[0-35-9])\d{8}$`, mobile)
if ok {
return true
}
return false
}

var trans ut.Translator

func main() {
r := gin.Default()

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
// "User.Mobile": "Mobile为必填字段",将Mobile为必填字段改为小写
v.RegisterTagNameFunc(func(field reflect.StructField) string {
name := strings.SplitN(field.Tag.Get("json"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})

zhT := zh.New() //chinese
uni := ut.New(zhT, zhT)

var found bool
trans, found = uni.GetTranslator("zh")
if !found {
log.Fatalln("zh not found")
}
_ = chTranslations.RegisterDefaultTranslations(v, trans)

_ = v.RegisterValidation("mobile", ValidateMobile)
_ = v.RegisterTranslation("mobile", trans, func(ut ut.Translator) error {
return ut.Add("mobile", "{0} 非法的手机号", false)
}, func(ut ut.Translator, fe validator.FieldError) string {
t, _ := ut.T("mobile", fe.Field())
return t
})
}

r.POST("", Login)
r.Run(":8080")
}

func Login(c *gin.Context) {
var u User
err := c.ShouldBind(&u) // 当密码为空,返回200,返回值为空
if err != nil {
ValidatorError(c, err)
return
}

c.JSON(http.StatusOK, u)
return
}

func ValidatorError(c *gin.Context, err error) {
errs, ok := err.(validator.ValidationErrors)
if !ok {
c.JSON(http.StatusOK, gin.H{
"message": err.Error(),
})
}
// 将错误信息返回给前端
c.JSON(http.StatusOK, gin.H{
// 错误封装
"error": removeTopStruct(errs.Translate(trans)),
})
return
}

// 通过将k中的字符串通过.切割,取最后一个,获取对应结构体名称
func removeTopStruct(field map[string]string) map[string]string {
resp := make(map[string]string, len(field))
for k, v := range field {
ks := strings.Split(k, ".")
resp[ks[len(ks)-1]] = v
}
return resp
}

参考文档:gin框架对前端传递过来的数据进行校验