1. validator 字段校验
在web请求中,常常需要对表单进行严格数据校验,否非常容易引发各种报错甚至panic,一般都是使用 validator 进行校验。validator 包含了大部分使用场景中的约束需求,比如字段是否必填、长度、大小、类型等等。对于一些特定需求,比如涉及到数据库查询,或者其它不满足的情况,可以通过自定义函数来实现校验。注意:只能校验对外可见的字段。
1.1. 使用内置约束
1.1.1. 常用的内置约束
操作符:- 不验证当前字段| or操作符,只要满足一个即可required 不能为零值omitempty 忽略空值,需要放在第一位范围约束:eq 等于。针对字符串和数字验证值相等,针对数组、切片和map验证元素个数ne 不等于。针对字符串和数字验证值相等,针对数组、切片和map验证元素个数gt 大于。针对数字比较值,字符串比较长度,针对数组、切片和map验证元素个数gte 大于等于。针对数字比较值,字符串比较长度,针对数组、切片和map验证元素个数lte 小于。针对数字比较值,字符串比较长度,针对数组、切片和map验证元素个数lte 小于等于。针对数字比较值,字符串比较长度,针对数组、切片和map验证元素个数len 长度。针对数字和时间(如30m)比较值,字符串比较长度,针对数组、切片和map验证元素个数。max 最大值。针对数字和时间(如30m)比较值,字符串比较长度,针对数组、切片和map验证元素个数。min 最小值。针对数字和时间(如30m)比较值,字符串比较长度,针对数组、切片和map验证元素个数。oneof 枚举。仅用于字符串和数字,用空格分割不同的元素,包含空格的字符串用单引号引用字符串约束:contains 包含指定的字符串excludes 不包含指定的字符串startswith 以指定字符串开头endwith 以指定的字符串结尾ascii 要求为ascii字符alpha 仅包含ascii的字母码字符alphanum 仅包含字母和数字lowercase 全部为小写字符,空字符也不符合要求uppercase 全部为大写字符,空字符也不符合要求json 有效的json字符串base64 要求为base64格式字符串,空字符也不符合要求uuid 要求为uuid格式datetime 要求为时间格式的字符串,采用golang中的format,如datetime=2006-01-02唯一约束:uniqid 当前切片、属组中没有重复值,map中没有重复key地址:email 要求为邮件地址格式url 要求为url格式uri 要求为uri格式字段比较:eqfield 当前字段值与某个字段值相同,如密码确认的场景nefield 当前字段值与某个字段值不同系统:file 校验文件是否存在,使用os.Stat校验的dir 校验目录是否存在,使用os.Stat校验的ip 要求为合法的ip地址,对ipv4地址使用ipv4cidr 要求为合法的CIDR地址tcp_addr 要求为合法的tcp地址upd_addr 要求为合法的udp地址
1.1.2. 案例
package mainimport ("github.com/go-playground/validator/v10""go_learn/logger""time")var validate = validator.New()type Host struct {UUID string `validate:"uuid|uuid3|uuid4|uuid5"`IPv4 string `validate:"omitempty,ipv4"`IPv6 string `validate:"omitempty,ipv6"`Mac string `validate:"mac"`GroupName string `validate:"-"`CPU int `validate:"gte=1"`Memory int `validate:"gte=512"`DiskType string `validate:"oneof=ssd hdd"`RaidType int `validate:"oneof=-1 0 1 5 10 50"`DiskSize int `validate:"gte=50"`MonitorUrL string `validate:"omitempty,url"`RegTime time.Time `validate:"required"`}func main() {h1 := &Host{UUID: "ac829fb8-e91f-498b-9d17-9f66fa6135d3",IPv4: "10.4.7.101",IPv6: "fe80::215:5dff:fed8:3601",Mac: "00:15:5d:d8:36:01",CPU: 2,Memory: 4096,DiskType: "hdd",RaidType: -1,DiskSize: 50,MonitorUrL: "http://xxx.grafana.com",RegTime: time.Now(),}h2 := Host{UUID: "ac829fb8-e91f-498b-9d17-9f66fa6135d3",Mac: "00:15:5d:d8:36:01",Memory: 128,RaidType: 2,}if err := validate.Struct(h1); err != nil {logger.Errorf("h1 error:%s", err.Error())}if err := validate.Struct(h2); err != nil {logger.Errorf("h2 error:%s", err.Error())}}
[root@duduniao check]# go run simple.go2021-01-16 20:43:22.599|h2 error:Key: 'Host.CPU' Error:Field validation for 'CPU' failed on the 'gte' tagKey: 'Host.Memory' Error:Field validation for 'Memory' failed on the 'gte' tagKey: 'Host.DiskType' Error:Field validation for 'DiskType' failed on the 'oneof' tagKey: 'Host.RaidType' Error:Field validation for 'RaidType' failed on the 'oneof' tagKey: 'Host.DiskSize' Error:Field validation for 'DiskSize' failed on the 'gte' tagKey: 'Host.RegTime' Error:Field validation for 'RegTime' failed on the 'required' tag
1.2. 自定义约束
上面内置类型能解决大部分场景下的验证问题,但是部分场景中的验证并不能满足,比如用户想要创建某项资源,但是数据库中已经存在相同ID的资源了。一般有两种解决方案:在validate()执行后,通过一个很函数或者方法去校验。另一种是将这个验证过程放在validate()中做掉。这里演示后者的使用!
在校验字段的过程中,需要获取结构体全部字段,此时需要调用 Top 方法,但是在类型断言时,一定要避免 panic
package mainimport ("fmt""github.com/go-playground/validator/v10")const (nameExist = "name_exist"classExist = "class_exist"sid = "sid")var validate = validator.New()func init() {// 注册到validate中_ = validate.RegisterValidation(sid, checkSidUniq)_ = validate.RegisterValidation(nameExist, checkNameExist)_ = validate.RegisterValidation(classExist, checkClassExist)}type Student struct {ID string `validate:"sid"`Name string `validate:"name_exist"`Class int `validate:"class_exist"`Score map[string]int `validate:"-"`}func main() {s1 := Student{ID: "s100", Class: 301, Name: "ZhangSan"}s2 := Student{ID: "s101", Class: 301, Name: "LiSi"}if err := validate.Struct(s1); err != nil {fmt.Printf("student s1 field invalid, err:%s\n", err.Error())return}if err := validate.Struct(s2); err != nil {fmt.Printf("student s2 field invalid, err:%s\n", err.Error())return}}func checkSidUniq(field validator.FieldLevel) bool {// 获取当前字段内容,即学生的IDid := field.Field().String()// 一般通过学生信息表,该学生ID是否存在,不存在则不合法,此处不做演示if id != "" {return true}return false}func checkClassExist(field validator.FieldLevel) bool {// 检查班级是否存在class := field.Field().Int()// 查询class表,是否存在当前的班级编号,此处不做演示if class >= 301 && class <= 309 {return true}return false}func checkNameExist(field validator.FieldLevel) bool {// 获取班级,并根据班级和学生名称检查该学生是否为当前班级内学生// 获取到结构体信息, 此处一定要判断类型,避免panicstudent, ok := field.Top().Interface().(Student)if !ok {return false}class := student.Classname := student.Namereturn GetStudentFormClass(class, name)}func GetStudentFormClass(class int, studentName string) bool {fmt.Printf("class:%d;name:%s\n", class, studentName)if class==301 && studentName == "LiSi" {return false}return true}
[root@duduniao check]# go run customize.go2021-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一致,针对自定义类型的字段,注册方式有所不同!
// types/host.gopackage typesimport ("encoding/json""github.com/go-playground/validator/v10""time")type Host struct {UUID string `json:"uuid" binding:"required,uuid|uuid3|uuid4|uuid5,new_uuid"`IPv4 string `json:"ipv4" binding:"required,ipv4"`IPv6 string `json:"ipv6,omitempty" binding:"omitempty,ipv6"`Mac string `json:"mac" binding:"required,mac"`GroupName string `json:"group_name,omitempty" binding:"-"`CPU int `json:"cpu" binding:"required,gte=1"`Memory int `json:"memory" binding:"required,gte=512"`DiskType string `json:"disk_type" binding:"required,oneof=ssd hdd"`RaidType int `json:"raid_type" binding:"required,oneof=-1 0 1 5 10 50"`DiskSize int `json:"disk_size" binding:"required,gte=50"`MonitorUrL string `json:"monitor_url,omitempty" binding:"omitempty,url"`RegTime time.Time `json:"reg_time" binding:"required"`}// NewUUIDCheck 检查uuid是否合法,一般是检查是否重复func NewUUIDCheck(fl validator.FieldLevel) bool {if fl.Field().String() == "4fd067e7-8c9c-4a65-a27a-95fa9432e9f4" {return false}return true}// MarshalToJson 构造json字符串func MarshalToJson() (string,error){host := &Host{UUID: "4fd067e7-8c9c-4a65-a27a-95fa9432e9f4",IPv4: "172.24.20.2",Mac: "02:42:20:3c:74:38",CPU: 4,Memory: 16384,DiskType: "ssd",RaidType: -1,DiskSize: 500,RegTime: time.Now(),}marshal, err := json.Marshal(host)return string(marshal), err}
// main.gopackage mainimport ("github.com/gin-gonic/gin""github.com/gin-gonic/gin/binding""github.com/go-playground/validator/v10""learn/validate/gin/types")func main() {httpServer()}func httpServer() {gin.SetMode(gin.ReleaseMode)r := gin.New()// 注册字段校验函数if validate, ok := binding.Validator.Engine().(*validator.Validate); ok {_ = validate.RegisterValidation("new_uuid", types.NewUUIDCheck)}r.POST("/", handler)err := r.Run("0.0.0.0:8080")if err != nil {panic(err)}}func handler(c *gin.Context) {var host types.Hosterr:= c.BindJSON(&host)if err != nil {c.JSON(400, "binding request body failed,err:"+err.Error())return}c.JSON(200, "ok")}
