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地址使用ipv4
cidr 要求为合法的CIDR地址
tcp_addr 要求为合法的tcp地址
upd_addr 要求为合法的udp地址
1.1.2. 案例
package main
import (
"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.go
2021-01-16 20:43:22.599|h2 error:Key: 'Host.CPU' Error:Field validation for 'CPU' failed on the 'gte' tag
Key: 'Host.Memory' Error:Field validation for 'Memory' failed on the 'gte' tag
Key: 'Host.DiskType' Error:Field validation for 'DiskType' failed on the 'oneof' tag
Key: 'Host.RaidType' Error:Field validation for 'RaidType' failed on the 'oneof' tag
Key: 'Host.DiskSize' Error:Field validation for 'DiskSize' failed on the 'gte' tag
Key: 'Host.RegTime' Error:Field validation for 'RegTime' failed on the 'required' tag
1.2. 自定义约束
上面内置类型能解决大部分场景下的验证问题,但是部分场景中的验证并不能满足,比如用户想要创建某项资源,但是数据库中已经存在相同ID的资源了。一般有两种解决方案:在validate()执行后,通过一个很函数或者方法去校验。另一种是将这个验证过程放在validate()中做掉。这里演示后者的使用!
在校验字段的过程中,需要获取结构体全部字段,此时需要调用 Top
方法,但是在类型断言时,一定要避免 panic
package main
import (
"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 {
// 获取当前字段内容,即学生的ID
id := 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 {
// 获取班级,并根据班级和学生名称检查该学生是否为当前班级内学生
// 获取到结构体信息, 此处一定要判断类型,避免panic
student, ok := field.Top().Interface().(Student)
if !ok {
return false
}
class := student.Class
name := student.Name
return 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.go
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一致,针对自定义类型的字段,注册方式有所不同!
// types/host.go
package types
import (
"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.go
package main
import (
"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.Host
err:= c.BindJSON(&host)
if err != nil {
c.JSON(400, "binding request body failed,err:"+err.Error())
return
}
c.JSON(200, "ok")
}