【Validator】字段验证器struct与多层级验证,go案例

标签用法总结表

标签 功能 代码实例
required 字段必填 Name string \v:"required"``
alphaunicode 验证字段是否只包含字母和 Unicode 字符 Name string \v:"alphaunicode"``
gte 验证字段值是否大于等于指定值 Age uint8 \v:"gte=10"``
lte 验证字段值是否小于等于指定值 Age uint8 \v:"lte=130"``
e164 验证电话号码是否符合 E.164 国际格式 Phone string \v:"e164"``
email 验证字段是否为有效的电子邮件地址 Email string \v:"email"``
iscolor 验证字段是否为有效的颜色值(如 #ff0000) FavouriteColor1 string \v:"iscolor"``
hexcolor 验证字段是否为十六进制颜色值(如 #ffffff) FavouriteColor2 string \v:"hexcolor"``
`rgb rgba hsl
dive 对切片或 map 的每个元素进行递归验证 Hobby []string \v:"dive,required"``
keys 验证 map 的键值 Data map[string]string \v:"dive,keys,alpha,len=2,endkeys"``
required_without_all 如果指定字段都为空,则当前字段必须存在 Phone string \v:"required_without_all=Email Address,omitempty"``
omitempty 当字段为空时,跳过后续验证 Age uint8 \v:"omitempty,gte=10,lte=130"``
len 验证字段的长度是否等于指定值 Data map[string]string \v:"dive,keys,alpha,len=2,endkeys"\ ``
endkeys 结束对 map 键的验证 Data map[string]string \v:"dive,keys,alpha,len=2,endkeys,required"``

标签参考:https://github.com/go-playground/validator?tab=readme-ov-file


示例代码

go 复制代码
package validate

import (
	"errors"
	"fmt"
	"github.com/go-playground/validator/v10"
)

type User struct { // validate.SetTagName("v")在这里,结构体的标签开头起作用
	Name            string            `v:"required, alphaunicode"`
	Age             uint8             `v:"gte=10, lte=130"` // gte=10: 年龄必须大于等于 10。lte=130: 年龄必须小于等于 130。
	Phone           string            `v:"required,e164"`   // e164: 电话号码必须符合 E.164 格式(国际电话号码格式)
	Email           string            `v:"required,email"`
	FavouriteColor1 string            `v:"iscolor"`                    // 必须是一个有效的颜色值(例如:#ff0000)
	FavouriteColor2 string            `v:"hexcolor|rgb|rgba|hsl|hsla"` // 必须是十六进制颜色、RGB、RGBA、HSL 或 HSLA 颜色格式之一
	Address         *Address          `v:"required"`
	ContactUser     []*ContactUser    `v:"required,gte=1,dive"`                                                      // dive: 对列表中的每个元素进行递归验证,后面如果继续跟逻辑运算,则处理对象是dive递归的元素
	Hobby           []string          `v:"required,gte=2,dive,required,gte=2,alphaunicode"`                          // dive: 对列表中的每个元素进行递归验证
	Data            map[string]string `v:"required,gte=2,dive,keys,alpha,len=2,endkeys,required,gte=2,alphaunicode"` // keys,alpha,len=2: 键必须是字母且长度为 2。 endkeys: 结束对键的验证
	// required,gte=2,alphaunicode 值不能为空且必须包含至少 2 个字符,且只能是字母、数字或 Unicode 字符
}

type ContactUser struct {
	Name    string   `v:"required,alphaunicode"`
	Age     uint8    `v:"gte=10,lte=130"`                                     // omitempty,代表当字段为空,不会出现在输出中
	Phone   string   `v:"required_without_all=Email Address,omitempty,e164"`  // required_without_all 无需全部,即 如果 Phone 和 Email 都为空,则 Phone 和 Email 都必须填写,否则,Phone 可以为空。
	Email   string   `v:"required_without_all=Phone Address,omitempty,email"` //  如果 Phone 和 Address 字段都为空,则 Email 必须存在;否则,Email 可以为空。 email 必须为email
	Address *Address `v:"required_without_all=Phone Email"`
}

type Address struct {
	Province string `v:"required"`
	City     string `v:"required"`
}

func StructValidate() {
	addr := &Address{
		Province: "湖南",
		City:     "长沙",
	}
	contactUser1 := &ContactUser{
		Name:  "nick",
		Age:   18,
		Phone: "+861380013800",
	}
	contactUser2 := &ContactUser{
		Name:  "张三",
		Age:   18,
		Email: "nick@0voice.com",
	}
	user := &User{
		Name:            "nick",
		Age:             18,
		Phone:           "+861380013800",
		Email:           "nick@0voice.com",
		FavouriteColor1: "#ffff",
		FavouriteColor2: "rgb(255,255,255)",
		Address:         addr,
		ContactUser:     []*ContactUser{contactUser1, contactUser2},
		Hobby:           []string{"篮球", "羽毛球"},
		Data:            map[string]string{"AB": "篮球", "CD": "羽毛球"},
	}
	err := validate.Struct(user)
	if err != nil {
		//if errors, ok := err.(validator.ValidationErrors); ok {
		// 错误的原因在于 validate.Struct(user) 返回的错误并不是直接的
		//validator.ValidationErrors 类型,而是可能被包装在其他类型的错误中。
		//因此,直接进行类型断言会失败。

		// 使用 errors.As 函数来检查是否包含 validator.ValidationErrors 类型的错误。
		var validationErrors validator.ValidationErrors
		if errors.As(err, &validationErrors) { // errors.As 函数的第二个参数需要是一个指针,指向一个可以接收错误类型的变量。
			// 因此,一定要先var一个可以接受的,然后再使用errors.As,而不是直接if errors.As(err, &validator.ValidationErrors)
			for _, err := range validationErrors {
				fmt.Println(err)
			}
		} else {
			fmt.Println("Validation failed with unknown error:", err)
		}
	}
}

示例 1: 必填字段验证
go 复制代码
Name string `v:"required,alphaunicode"`
  • required:Name 字段不能为空。
  • alphaunicode:Name 只能包含字母或 Unicode 字符。

示例 2: 数值范围验证
go 复制代码
Age uint8 `v:"gte=10,lte=130"`
  • gte=10:Age 必须大于等于 10。
  • lte=130:Age 必须小于等于 130。

示例 3: 列表递归验证
go 复制代码
Hobby []string `v:"required,gte=2,dive,required,gte=2,alphaunicode"`
  • required:Hobby 列表不能为空。
  • gte=2:Hobby 列表必须包含至少 2 个元素。
  • dive:对列表中的每个元素进行递归验证。
  • required,gte=2,alphaunicode:列表中的每个元素必须非空,且至少为 2 个字符,且只包含字母和 Unicode 字符。

示例 4: Map 验证
go 复制代码
Data map[string]string `v:"required,gte=2,dive,keys,alpha,len=2,endkeys,required,gte=2,alphaunicode"`
  • required,gte=2:Data 必须有至少 2 个键值对。
  • dive:递归验证 map 的每个键和值。
  • keys,alpha,len=2,endkeys:键必须为字母,且长度为 2。
  • required,gte=2,alphaunicode:值不能为空,至少为 2 个字符,且只能包含字母和 Unicode 字符。

Go 语言中 omitempty 和 required_without_all 的合理使用

1. omitempty 标签的作用

omitempty 是 json 标签的一个选项,用于控制 JSON 序列化时的行为。当字段的值为零值(如空字符串、0、nil 等)时,该字段将被忽略,不会出现在生成的 JSON 输出中。

示例:

go 复制代码
type User struct {
    Name string `json:"name,omitempty"`
    Age  int    `json:"age,omitempty"`
}
  • 如果 Name 或 Age 字段为空或为零值,则它们不会出现在 JSON 输出中。
go 复制代码
user := User{
    Name: "Alice",
    Age:  0,
}

jsonData, _ := json.Marshal(user)
fmt.Println(string(jsonData)) // 输出: {"name":"Alice"}
2. required_without_all 标签的作用

required_without_all 是 validator 标签的一部分,用于条件验证。它表示当指定的其他字段都为空时,当前字段必须存在且不为空。

示例:

go 复制代码
type ContactUser struct {
    Phone   string `v:"required_without_all=Email Address,e164"`
    Email   string `v:"required_without_all=Phone Address,email"`
    Address *Address `v:"required_without_all=Phone Email"`
}
  • 如果 Phone 和 Email 都为空,则 Phone 必须存在且符合 E.164 格式。
  • 如果 Phone 和 Address 都为空,则 Email 必须存在且是有效的电子邮件地址。
  • 如果 Phone 和 Email 字段有一个非空,则另一个字段可以为空。
3. omitempty 和 required_without_all 的组合使用

这两个标签可以共存,但需要注意它们分别作用于不同的阶段:

  • required_without_all 用于数据验证,确保在特定条件下字段不为空。
  • omitempty 用于 JSON 序列化,确保零值字段不会出现在输出中。

示例:

go 复制代码
type ContactUser struct {
    Name    string   `json:"name,omitempty" v:"required,alphaunicode"`
    Age     uint8    `json:"age,omitempty" v:"gte=10,lte=130"`
    Phone   string   `json:"phone,omitempty" v:"required_without_all=Email Address,e164"`
    Email   string   `json:"email,omitempty" v:"required_without_all=Phone Address,email"`
    Address *Address `json:"address,omitempty" v:"required_without_all=Phone Email"`
}
  • 验证逻辑 :required_without_all 确保在特定条件下字段不为空。
  • JSON 序列化 :omitempty 确保在 JSON 序列化时,零值字段不会出现在输出中。
4. 最佳实践建议

为了提高代码的可读性和维护性,建议将 json 标签和 validator 标签分开写:

go 复制代码
type ContactUser struct {
    Name    string   `json:"name,omitempty" v:"required,alphaunicode"`
    Age     uint8    `json:"age,omitempty" v:"gte=10,lte=130"`
    Phone   string   `json:"phone,omitempty" v:"required_without_all=Email Address,e164"`
    Email   string   `json:"email,omitempty" v:"required_without_all=Phone Address,email"`
    Address *Address `json:"address,omitempty" v:"required_without_all=Phone Email"`
}

多层struct的go案例讲解

go 复制代码
package main

import (
	"errors"
	"fmt"
	"github.com/go-playground/validator/v10"
)

// 嵌套的地址结构
type Address struct {
	Street  string `validate:"required"`       // 街道名称必填
	City    string `validate:"required,alpha"` // 城市名称必填,只能包含字母
	ZipCode string `validate:"required,len=5"` // 邮编必填,且长度必须为 5
}

// 嵌套的联系人结构
type Contact struct {
	Name    string  `validate:"required,alphaunicode"`        // 名称必填,支持字母和 Unicode 字符
	Phone   string  `validate:"required_without=Email,e164"`  // 电话和邮箱至少填一个,且电话需符合 E.164 格式
	Email   string  `validate:"required_without=Phone,email"` // 如果没有填写电话,邮箱必须是有效的邮件地址
	Address Address `validate:"required,dive"`                // 验证嵌套的地址字段
}

// 主用户结构
type User struct {
	ID       int       `validate:"required,gte=1"`                   // 用户 ID 必填,且必须大于等于 1
	Username string    `validate:"required,alphanum"`                // 用户名必填,只能包含字母和数字
	Contacts []Contact `validate:"required,gte=1,dive"`              // 至少包含一个联系人,每个联系人需通过验证
	Hobbies  []string  `validate:"required,gte=1,dive,alphaunicode"` // 至少有一个爱好,每个爱好只能包含字母和 Unicode 字符
}

var validate = validator.New()

func main() {
	// 构造测试数据
	user := User{
		ID:       101,
		Username: "john_doe",
		Contacts: []Contact{
			{
				Name:  "Alice",
				Phone: "+1234567890",
				Address: Address{
					Street:  "123 Main St",
					City:    "Springfield",
					ZipCode: "12345",
				},
			},
			{
				Name:  "Bob",
				Email: "bob@example.com",
				Address: Address{
					Street:  "456 Elm St",
					City:    "Shelbyville",
					ZipCode: "54321",
				},
			},
		},
		Hobbies: []string{"Reading", "Cooking"},
	}

	// 进行验证
	err := validate.Struct(user)
	if err != nil {
		// 使用 errors.As 检查是否包含 ValidationErrors
		var validationErrors validator.ValidationErrors // 底层是一个切片
		if errors.As(err, &validationErrors) {
			for _, fieldErr := range validationErrors {
				fmt.Printf("Field '%s' failed validation: %s\n", fieldErr.Field(), fieldErr.Tag())
			}
		} else {
			fmt.Println("Validation failed:", err)
		}
	} else {
		fmt.Println("All validations passed!")
	}
}
1. 嵌套结构
  • Address 和 Contact 是嵌套结构:
  • Address 包含 Street、City 和 ZipCode,验证逻辑应用到每个字段。
  • Contact 包含 Name、Phone、Email 和嵌套的 Address,同时对 Phone 和 Email 设置了互斥验证。
2. 多层级验证
  • 一级验证 :User 的 ID 和 Username。
  • 二级验证 :User 的 Contacts 列表:
  • 验证列表中至少包含一个联系人(gte=1)。
  • 使用 dive,递归验证每个联系人。
  • 三级验证 :Contact 中的 Address:
  • 验证 Street、City 和 ZipCode。
3. 验证列表和递归验证
  • Contacts 使用 dive 标签递归验证列表内的每个元素。
  • 对 Hobbies 列表的验证中:
  • 使用 dive 验证列表中的每个爱好,确保它们符合 alphaunicode 的规则。
4. 动态互斥规则
  • Phone 和 Email 互斥规则:
  • Phone:required_without=Email,表示如果 Email 为空,Phone 必须存在。
  • Email:required_without=Phone,表示如果 Phone 为空,Email 必须是有效邮件地址。

https://github.com/0voice

相关推荐
不是万能的豆学长4 小时前
filetidy 文件整理助手:一键归类 + 规范重命名(Python 零依赖单文件工具)
大数据·开发语言·python·技术美术
懂点Tech8 小时前
ctfshow MISC入门图片篇(信息附加):misc41解题思路
开发语言·python
catchadmin9 小时前
PHP 8.7 协程新提案 让跨协程代码共享请求身份
开发语言·php
念何架构之路10 小时前
go-grpc设计思想
开发语言·后端·golang
酷炫码神10 小时前
Day 10|Pinia 状态管理:把散落在各组件的数据,收进一个全局仓库
android·开发语言·javascript
microrain10 小时前
SagooIoT 的缓存换后端只改一行配置,文件后端连 Redis 都不需要
物联网·缓存·golang·sagooiot
Elastic 中国社区官方博客11 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn18811 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn12 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
喵了几个咪12 小时前
GoWind ShortPlay 深度拆解:三服务 BFF + Core,一条请求的完整工程链路
golang·gowind