validator
起因是在使用gin框架的模型绑定的时候,发现它默认使用了go-playground/validator这个库来做数据验证
那我们先从gin的模型绑定说起
gin的模型绑定
什么是模型绑定?
即将请求的数据绑定到结构体上
我们看一个例子:
1// 添加一个shell task的请求
2type AddShellTaskRequest struct {
3 TaskName string `json:"task_name" binding:"required"` // 任务名称
4 Description string `json:"description" binding:"required"` // 任务描述
5 ScheduledTime string `json:"scheduled_time" binding:"required,cron"` // Cron表达式(支持秒级)
6 Command string `json:"command" binding:"required"` // 执行命令
7 Args []string `json:"args" binding:"omitempty" ` // 命令参数
8 Timeout int `json:"timeout" binding:"required,max=7200,gt=0"` // 超时时间(秒),最大2小时
9}
10// 例如前端传来一个Json的请求
11// {
12// "args": [
13// "backup.sh",
14// "--full"
15// ],
16// "command": "/bin/bash",
17// "description": "每日数据备份任务",
18// "scheduled_time": "0 2 * * * *",
19// "task_name": "daily-backup",
20// "timeout": 1800,
21// }
22
23// 我们需要把这个请求绑定到AddShellTaskRequest结构体上
24
25var req AddShellTaskRequest
26// 这个方法会自动帮我们把请求的数据绑定到req变量上,并且根据binding标签做数据验证
27// 如果验证失败,就会抛出错误
28if err := c.ShouldBindBodyWithJSON(&req); err != nil {
29 c.JSON(http.StatusBadRequest, map[string]string{
30 "error": err.Error(),
31 })
32 return
33}
可以发现,如果我们没有配置binding标签,模型绑定后我们需要手动去验证数据的合法性,这样会增加代码量,并且容易出错
但是如果我们配置了binding标签,模型绑定会自动帮我们做数据验证,这样就省去了手动验证的麻烦
而gin框架默认使用的验证库就是go-playground/validator,它是一个功能强大且灵活的Go语言数据验证库
这里顺便提一嘴,binging里面的标签,比如required都是可以从go-playground/validator的文档中找到的,这边后面会简单介绍一些常见的;而且,我们也可以自定义标签及验证函数
validator库的使用
验证单个字段变量值
这个虽然并不常见,但是也简单介绍下
1package main
2
3import (
4 "fmt"
5
6 "github.com/go-playground/validator/v10"
7)
8
9func main() {
10
11 //validator.New() 创建一个验证器
12 validate := validator.New()
13
14 // 这边主要还是传入tag来进行验证
15 // email: 用来检验字符串是否符合email的格式
16 // required : 字段必须设置,不能为默认值;对于数字,确保值不为零。对于字符串,确保值不为空字符串。对于布尔值,确保值不为 false;对于切片、映射、指针、接口、通道和函数,确保值不为 nil
17 // 这里需要对bool类型特别注意,如果你要绑定一个bool值,最好不要使用required标签,因为这样会被拦截掉false值
18 // 当然如果对于int类型来说,0是一个有效值,也不应该设置这个标签,他会拦截掉0值
19 var emailTest string = "test@126.com"
20 err = validate.Var(emailTest, "required,email")
21 if err != nil {
22 fmt.Println(err)
23 } else {
24 fmt.Println("success") // 输出: success
25 }
26
27 // gte=大于等于 (gt = 大于)
28 // lte=小于等于 (lt = 小于)
29 // eq = 等于
30 var a int
31 err = validate.Var()a, "gte=10,lte=20")
32 if err != nil {
33 fmt.Println(err)
34 } else {
35 fmt.Println("success") // 输出: success
36 }
37
38 // 这里也介绍下omitempty : 如果字段为零值,则跳过后续约束;否则继续执行后续约束
39 a = 0
40 err := validate.Var(a, "omitempty,min=18")
41 if err != nil {
42 fmt.Printf("a: %d failed: %v\n", a, err.Error())
43 } else {
44 fmt.Printf("a: %d success\n",a)
45 }
46
47 a = 18
48 err = validate.Var(a, "omitempty,min=18")
49 if err != nil {
50 fmt.Printf("a: %d failed: %v\n", a, err.Error())
51 }else {
52 fmt.Printf("a: %d success\n",a)
53 }
54
55 a = 7
56 err = validate.Var(a, "omitempty,min=18")
57 if err != nil {
58 fmt.Printf("a: %d failed: %v\n", a, err.Error())
59 }else {
60 fmt.Printf("a: %d success\n",a)
61 }
62
63 // 结果:
64 // a: 0 success
65 // a: 18 success
66 // a: 7 failed: Key: '' Error:Field validation for '' failed on the 'min' tag
67}
验证struct
1package main
2
3import (
4 "fmt"
5
6 "github.com/go-playground/validator/v10"
7)
8
9type User struct {
10 FirstName string `validate:"required"`
11 LastName string `validate:"required"`
12 Age uint8 `validate:"gte=0,lte=130"`
13 Email string `validate:"required,email"`
14 Addresses []*Address `validate:"required,dive,required"`
15}
16
17type Address struct {
18 Street string `validate:"required"`
19 City string `validate:"required"`
20 Planet string `validate:"required"`
21 Phone string `validate:"required"`
22}
23
24func main() {
25 address := &Address{
26 Street: "Eavesdown Docks",
27 Planet: "Persphone",
28 Phone: "none",
29 }
30
31 user := &User{
32 FirstName: "Badger",
33 LastName: "Smith",
34 Age: 135,
35 Email: "Badger.Smith@gmail.com",
36 Addresses: []*Address{address},
37 }
38
39 validate := validator.New()
40 // 对于结构体的验证,直接使用Struct方法
41 // 这里介绍下dive标签:用于切片、数组、映射和指针等复合类型,表示深入一层验证
42 err := validate.Struct(user)
43 if err != nil {
44 fmt.Println(err)
45 return
46 }
47
48 // 输出
49 // Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag
50 // Key: 'User.Addresses[0].City' Error:Field validation for 'City' failed on the 'required' tag
51}
自定义约束
除了使用validator提供的约束外,还可以定义自己的约束
例如现在有个需求,要求用户必须使用回文串作为用户名,我们可以自定义这个约束:
1package main
2
3import (
4 "fmt"
5
6 "github.com/go-playground/validator/v10"
7)
8
9type RegisterForm struct {
10 Name string `validate:"palindrome"`
11 Age int `validate:"min=18"`
12}
13
14func reverseString(s string) string {
15 runes := []rune(s)
16 for from, to := 0, len(runes)-1; from < to; from, to = from+1, to-1 {
17 runes[from], runes[to] = runes[to], runes[from]
18 }
19
20 return string(runes)
21}
22
23func CheckPalindrome(fl validator.FieldLevel) bool {
24 // fl.Field() 获取当前字段的反射的值(reflect.Value)
25 value := fl.Field().String()
26 return value == reverseString(value)
27}
28
29func main() {
30 validate := validator.New()
31 // 这边注册一个标签 "palindrome" 对应的验证函数CheckPalindrome
32 validate.RegisterValidation("palindrome", CheckPalindrome)
33
34 f1 := RegisterForm{
35 Name: "djd",
36 Age: 18,
37 }
38 err := validate.Struct(f1)
39 if err != nil {
40 fmt.Println(err)
41 } else {
42 fmt.Printf("f1 success\n")
43 }
44
45 f2 := RegisterForm{
46 Name: "dj",
47 Age: 18,
48 }
49 err = validate.Struct(f2)
50 if err != nil {
51 fmt.Println(err)
52 } else {
53 fmt.Printf("f2 success\n")
54 }
55
56 // 输出
57 // f1 success
58 // Key: 'RegisterForm.Name' Error:Field validation for 'Name' failed on the 'palindrome' tag
59}
常见约束
范围约束
范围约束的字段类型有以下几种:
- 对于数值,则约束其值
- 对于字符串,则约束其长度
- 对于切片、数组和map,则约束其长度
下面如未特殊说明,则是根据上面各个类型对应的值与参数值比较:
len:等于参数值,例如len=10;
max:小于等于参数值,例如max=10;
min:大于等于参数值,例如min=10;
eq:等于参数值,注意与len不同。对于字符串,eq约束字符串本身的值,而len约束字符串长度。例如eq=10;
ne:不等于参数值,例如ne=10;
gt:大于参数值,例如gt=10;
gte:大于等于参数值,例如gte=10;
lt:小于参数值,例如lt=10;
lte:小于等于参数值,例如lte=10;
oneof:只能是列举出的值其中一个,这些值必须是数值或字符串,以空格分隔,如果字符串中有空格,将字符串用单引号包围,例如oneof=red green
字符串约束
-
contains:包含参数子串,例如contains=email;
-
containsany:包含参数中任意的 UNICODE 字符,例如containsany=abcd;
-
excludes:不包含参数子串,例如excludes=email;
-
excludesall:不包含参数中任意的 UNICODE 字符,例如excludesall=abcd;
-
startswith:以参数子串为前缀,例如startswith=hello;
-
endswith:以参数子串为后缀,例如endswith=bye
唯一性
使用unqiue来指定唯一性约束,对不同类型的处理如下:
特殊
- : 跳过该字段,不检验
| : 使用多个约束,只需要满足其中一个,例如rgb|rgba;
required : 字段必须设置,不能为默认值;对于数字,确保值不为零。对于字符串,确保值不为空字符串。对于布尔值,确保值不为 false;对于切片、映射、指针、接口、通道和函数,确保值不为 nil
omitempty : 如果字段为零值,则跳过后续约束;否则继续执行后续约束
跨字段约束
validator允许定义跨字段的约束,即该字段与其他字段之间的关系
这种约束实际上分为两种:
- 参数字段就是同一个结构中的平级字段
- 参数字段为结构中其他字段的字段
如果是约束同一个结构中的字段,使用eqfield,例如 eqfield=ConfirmPassword
如果是更深层次的字段,使用eqcsfield,例如 eqcsfield=InnerStructField.Field
例如:
1type RegisterForm struct {
2 Name string `validate:"min=2"`
3 Age int `validate:"min=18"`
4 Password string `validate:"min=10"`
5 ConfirmPassword string `validate:"eqfield=Password"` // ConfirmPassword必须和Password字段相等
6}