Golang学习笔记(08-2-3-字段校验)

538 阅读6分钟

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")
}