1. validator 字段校验

在web请求中,常常需要对表单进行严格数据校验,否非常容易引发各种报错甚至panic,一般都是使用 validator 进行校验。validator 包含了大部分使用场景中的约束需求,比如字段是否必填、长度、大小、类型等等。对于一些特定需求,比如涉及到数据库查询,或者其它不满足的情况,可以通过自定义函数来实现校验。注意:只能校验对外可见的字段。

1.1. 使用内置约束

1.1.1. 常用的内置约束

所有内置约束:代码文档

  1. 操作符:
  2. - 不验证当前字段
  3. | or操作符,只要满足一个即可
  4. required 不能为零值
  5. omitempty 忽略空值,需要放在第一位
  6. 范围约束:
  7. eq 等于。针对字符串和数字验证值相等,针对数组、切片和map验证元素个数
  8. ne 不等于。针对字符串和数字验证值相等,针对数组、切片和map验证元素个数
  9. gt 大于。针对数字比较值,字符串比较长度,针对数组、切片和map验证元素个数
  10. gte 大于等于。针对数字比较值,字符串比较长度,针对数组、切片和map验证元素个数
  11. lte 小于。针对数字比较值,字符串比较长度,针对数组、切片和map验证元素个数
  12. lte 小于等于。针对数字比较值,字符串比较长度,针对数组、切片和map验证元素个数
  13. len 长度。针对数字和时间(如30m)比较值,字符串比较长度,针对数组、切片和map验证元素个数。
  14. max 最大值。针对数字和时间(如30m)比较值,字符串比较长度,针对数组、切片和map验证元素个数。
  15. min 最小值。针对数字和时间(如30m)比较值,字符串比较长度,针对数组、切片和map验证元素个数。
  16. oneof 枚举。仅用于字符串和数字,用空格分割不同的元素,包含空格的字符串用单引号引用
  17. 字符串约束:
  18. contains 包含指定的字符串
  19. excludes 不包含指定的字符串
  20. startswith 以指定字符串开头
  21. endwith 以指定的字符串结尾
  22. ascii 要求为ascii字符
  23. alpha 仅包含ascii的字母码字符
  24. alphanum 仅包含字母和数字
  25. lowercase 全部为小写字符,空字符也不符合要求
  26. uppercase 全部为大写字符,空字符也不符合要求
  27. json 有效的json字符串
  28. base64 要求为base64格式字符串,空字符也不符合要求
  29. uuid 要求为uuid格式
  30. datetime 要求为时间格式的字符串,采用golang中的format,如datetime=2006-01-02
  31. 唯一约束:
  32. uniqid 当前切片、属组中没有重复值,map中没有重复key
  33. 地址:
  34. email 要求为邮件地址格式
  35. url 要求为url格式
  36. uri 要求为uri格式
  37. 字段比较:
  38. eqfield 当前字段值与某个字段值相同,如密码确认的场景
  39. nefield 当前字段值与某个字段值不同
  40. 系统:
  41. file 校验文件是否存在,使用os.Stat校验的
  42. dir 校验目录是否存在,使用os.Stat校验的
  43. ip 要求为合法的ip地址,对ipv4地址使用ipv4
  44. cidr 要求为合法的CIDR地址
  45. tcp_addr 要求为合法的tcp地址
  46. upd_addr 要求为合法的udp地址

1.1.2. 案例

  1. package main
  2. import (
  3. "github.com/go-playground/validator/v10"
  4. "go_learn/logger"
  5. "time"
  6. )
  7. var validate = validator.New()
  8. type Host struct {
  9. UUID string `validate:"uuid|uuid3|uuid4|uuid5"`
  10. IPv4 string `validate:"omitempty,ipv4"`
  11. IPv6 string `validate:"omitempty,ipv6"`
  12. Mac string `validate:"mac"`
  13. GroupName string `validate:"-"`
  14. CPU int `validate:"gte=1"`
  15. Memory int `validate:"gte=512"`
  16. DiskType string `validate:"oneof=ssd hdd"`
  17. RaidType int `validate:"oneof=-1 0 1 5 10 50"`
  18. DiskSize int `validate:"gte=50"`
  19. MonitorUrL string `validate:"omitempty,url"`
  20. RegTime time.Time `validate:"required"`
  21. }
  22. func main() {
  23. h1 := &Host{
  24. UUID: "ac829fb8-e91f-498b-9d17-9f66fa6135d3",
  25. IPv4: "10.4.7.101",
  26. IPv6: "fe80::215:5dff:fed8:3601",
  27. Mac: "00:15:5d:d8:36:01",
  28. CPU: 2,
  29. Memory: 4096,
  30. DiskType: "hdd",
  31. RaidType: -1,
  32. DiskSize: 50,
  33. MonitorUrL: "http://xxx.grafana.com",
  34. RegTime: time.Now(),
  35. }
  36. h2 := Host{
  37. UUID: "ac829fb8-e91f-498b-9d17-9f66fa6135d3",
  38. Mac: "00:15:5d:d8:36:01",
  39. Memory: 128,
  40. RaidType: 2,
  41. }
  42. if err := validate.Struct(h1); err != nil {
  43. logger.Errorf("h1 error:%s", err.Error())
  44. }
  45. if err := validate.Struct(h2); err != nil {
  46. logger.Errorf("h2 error:%s", err.Error())
  47. }
  48. }
  1. [root@duduniao check]# go run simple.go
  2. 2021-01-16 20:43:22.599|h2 error:Key: 'Host.CPU' Error:Field validation for 'CPU' failed on the 'gte' tag
  3. Key: 'Host.Memory' Error:Field validation for 'Memory' failed on the 'gte' tag
  4. Key: 'Host.DiskType' Error:Field validation for 'DiskType' failed on the 'oneof' tag
  5. Key: 'Host.RaidType' Error:Field validation for 'RaidType' failed on the 'oneof' tag
  6. Key: 'Host.DiskSize' Error:Field validation for 'DiskSize' failed on the 'gte' tag
  7. Key: 'Host.RegTime' Error:Field validation for 'RegTime' failed on the 'required' tag

1.2. 自定义约束

上面内置类型能解决大部分场景下的验证问题,但是部分场景中的验证并不能满足,比如用户想要创建某项资源,但是数据库中已经存在相同ID的资源了。一般有两种解决方案:在validate()执行后,通过一个很函数或者方法去校验。另一种是将这个验证过程放在validate()中做掉。这里演示后者的使用!
在校验字段的过程中,需要获取结构体全部字段,此时需要调用 Top 方法,但是在类型断言时,一定要避免 panic

  1. package main
  2. import (
  3. "fmt"
  4. "github.com/go-playground/validator/v10"
  5. )
  6. const (
  7. nameExist = "name_exist"
  8. classExist = "class_exist"
  9. sid = "sid"
  10. )
  11. var validate = validator.New()
  12. func init() {
  13. // 注册到validate中
  14. _ = validate.RegisterValidation(sid, checkSidUniq)
  15. _ = validate.RegisterValidation(nameExist, checkNameExist)
  16. _ = validate.RegisterValidation(classExist, checkClassExist)
  17. }
  18. type Student struct {
  19. ID string `validate:"sid"`
  20. Name string `validate:"name_exist"`
  21. Class int `validate:"class_exist"`
  22. Score map[string]int `validate:"-"`
  23. }
  24. func main() {
  25. s1 := Student{ID: "s100", Class: 301, Name: "ZhangSan"}
  26. s2 := Student{ID: "s101", Class: 301, Name: "LiSi"}
  27. if err := validate.Struct(s1); err != nil {
  28. fmt.Printf("student s1 field invalid, err:%s\n", err.Error())
  29. return
  30. }
  31. if err := validate.Struct(s2); err != nil {
  32. fmt.Printf("student s2 field invalid, err:%s\n", err.Error())
  33. return
  34. }
  35. }
  36. func checkSidUniq(field validator.FieldLevel) bool {
  37. // 获取当前字段内容,即学生的ID
  38. id := field.Field().String()
  39. // 一般通过学生信息表,该学生ID是否存在,不存在则不合法,此处不做演示
  40. if id != "" {
  41. return true
  42. }
  43. return false
  44. }
  45. func checkClassExist(field validator.FieldLevel) bool {
  46. // 检查班级是否存在
  47. class := field.Field().Int()
  48. // 查询class表,是否存在当前的班级编号,此处不做演示
  49. if class >= 301 && class <= 309 {
  50. return true
  51. }
  52. return false
  53. }
  54. func checkNameExist(field validator.FieldLevel) bool {
  55. // 获取班级,并根据班级和学生名称检查该学生是否为当前班级内学生
  56. // 获取到结构体信息, 此处一定要判断类型,避免panic
  57. student, ok := field.Top().Interface().(Student)
  58. if !ok {
  59. return false
  60. }
  61. class := student.Class
  62. name := student.Name
  63. return GetStudentFormClass(class, name)
  64. }
  65. func GetStudentFormClass(class int, studentName string) bool {
  66. fmt.Printf("class:%d;name:%s\n", class, studentName)
  67. if class==301 && studentName == "LiSi" {
  68. return false
  69. }
  70. return true
  71. }
  1. [root@duduniao check]# go run customize.go
  2. 2021-01-16 21:43:51.315|student s1 field invalid, err:Key: 'Student.ID' Error:Field validation for 'ID' failed on the 'sid' tag

2. Gin框架中字段校验

Gin框架内部封装了validatro包,在执行绑定方法时,就会自动校验字段是否符合要求,对应tag是 binding ,一般的字段校验和validator一致,针对自定义类型的字段,注册方式有所不同!

  1. // types/host.go
  2. package types
  3. import (
  4. "encoding/json"
  5. "github.com/go-playground/validator/v10"
  6. "time"
  7. )
  8. type Host struct {
  9. UUID string `json:"uuid" binding:"required,uuid|uuid3|uuid4|uuid5,new_uuid"`
  10. IPv4 string `json:"ipv4" binding:"required,ipv4"`
  11. IPv6 string `json:"ipv6,omitempty" binding:"omitempty,ipv6"`
  12. Mac string `json:"mac" binding:"required,mac"`
  13. GroupName string `json:"group_name,omitempty" binding:"-"`
  14. CPU int `json:"cpu" binding:"required,gte=1"`
  15. Memory int `json:"memory" binding:"required,gte=512"`
  16. DiskType string `json:"disk_type" binding:"required,oneof=ssd hdd"`
  17. RaidType int `json:"raid_type" binding:"required,oneof=-1 0 1 5 10 50"`
  18. DiskSize int `json:"disk_size" binding:"required,gte=50"`
  19. MonitorUrL string `json:"monitor_url,omitempty" binding:"omitempty,url"`
  20. RegTime time.Time `json:"reg_time" binding:"required"`
  21. }
  22. // NewUUIDCheck 检查uuid是否合法,一般是检查是否重复
  23. func NewUUIDCheck(fl validator.FieldLevel) bool {
  24. if fl.Field().String() == "4fd067e7-8c9c-4a65-a27a-95fa9432e9f4" {
  25. return false
  26. }
  27. return true
  28. }
  29. // MarshalToJson 构造json字符串
  30. func MarshalToJson() (string,error){
  31. host := &Host{
  32. UUID: "4fd067e7-8c9c-4a65-a27a-95fa9432e9f4",
  33. IPv4: "172.24.20.2",
  34. Mac: "02:42:20:3c:74:38",
  35. CPU: 4,
  36. Memory: 16384,
  37. DiskType: "ssd",
  38. RaidType: -1,
  39. DiskSize: 500,
  40. RegTime: time.Now(),
  41. }
  42. marshal, err := json.Marshal(host)
  43. return string(marshal), err
  44. }
  1. // main.go
  2. package main
  3. import (
  4. "github.com/gin-gonic/gin"
  5. "github.com/gin-gonic/gin/binding"
  6. "github.com/go-playground/validator/v10"
  7. "learn/validate/gin/types"
  8. )
  9. func main() {
  10. httpServer()
  11. }
  12. func httpServer() {
  13. gin.SetMode(gin.ReleaseMode)
  14. r := gin.New()
  15. // 注册字段校验函数
  16. if validate, ok := binding.Validator.Engine().(*validator.Validate); ok {
  17. _ = validate.RegisterValidation("new_uuid", types.NewUUIDCheck)
  18. }
  19. r.POST("/", handler)
  20. err := r.Run("0.0.0.0:8080")
  21. if err != nil {
  22. panic(err)
  23. }
  24. }
  25. func handler(c *gin.Context) {
  26. var host types.Host
  27. err:= c.BindJSON(&host)
  28. if err != nil {
  29. c.JSON(400, "binding request body failed,err:"+err.Error())
  30. return
  31. }
  32. c.JSON(200, "ok")
  33. }