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来指定唯一性约束,对不同类型的处理如下:

  • 对于数组和切片,unique约束没有重复的元素;

  • 对于map,unique约束没有重复的

  • 对于元素类型为结构体的切片,unique约束结构体对象的某个字段不重复,通过unqiue=field指定这个字段名

特殊

  • - : 跳过该字段,不检验
  • | : 使用多个约束,只需要满足其中一个,例如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}