【Validator】自定义字段、结构体补充及自定义验证,go案例讲解ReportError和errors.As在其中的使用
自定义字段名称的显示
RegisterTagNameFunc
,自定义字段名称的显示,以便于从字段标签(tag
)中提取更有意义的名称。
代码示例:自定义字段名称
package main
import (
"fmt"
"reflect"
"strings"
"github.com/go-playground/validator/v10"
)
type User struct {
FirstName string `json:"first_name" fld:"First Name"`
LastName string `json:"last_name" fld:"Last Name"`
Age uint8 `json:"age" v:"gte=0,lte=130" fld:"Age"`
Email string `json:"email" v:"required,email" fld:"Email Address"`
}
func main() {
validate := validator.New()
// 注册字段名称的自定义提取函数
validate.RegisterTagNameFunc(func(field reflect.StructField) string {
name := strings.SplitN(field.Tag.Get("fld"), ",", 2)[0]
if name == "-" {
return ""
}
return name
})
user := &User{
FirstName: "",
LastName: "",
Age: 150,
Email: "invalid-email",
}
err := validate.Struct(user)
if err != nil {
if validationErrors, ok := err.(validator.ValidationErrors); ok {
for _, fieldErr := range validationErrors {
fmt.Printf("Field: %s, Error: %s\n", fieldErr.Field(), fieldErr.Error())
}
} else {
fmt.Println("Validation failed with unknown error:", err)
}
}
}
输出结果
Field: First Name, Error: Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'required' tag
Field: Last Name, Error: Key: 'User.LastName' Error:Field validation for 'LastName' failed on the 'required' tag
Field: Age, Error: Key: 'User.Age' Error:Field validation for 'Age' failed on the 'lte' tag
Field: Email Address, Error: Key: 'User.Email' Error:Field validation for 'Email' failed on the 'email' tag
自定义结构体级别验证
结构体级别验证允许定义复杂的跨字段验证逻辑。例如,我们可以验证 FirstName
和 LastName
至少需要一个非空。
代码示例:结构体级别验证
func StructLevelValidation(sl validator.StructLevel) {
user := sl.Current().Interface().(User)
if user.FirstName == "" && user.LastName == "" {
sl.ReportError(user.FirstName, "FirstName", "FirstName", "requiredtogether", "")
sl.ReportError(user.LastName, "LastName", "LastName", "requiredtogether", "")
}
}
func main() {
validate := validator.New()
// 注册结构体级别验证
validate.RegisterStructValidation(StructLevelValidation, User)
user := &User{
FirstName: "",
LastName: "",
Age: 25,
Email: "valid@example.com",
}
err := validate.Struct(user)
if err != nil {
if validationErrors, ok := err.(validator.ValidationErrors); ok {
for _, fieldErr := range validationErrors {
fmt.Printf("Field: %s, Error: %s\n", fieldErr.Field(), fieldErr.Error())
}
} else {
fmt.Println("Validation failed with unknown error:", err)
}
}
}
输出结果
Field: FirstName, Error: Key: 'User.FirstName' Error:Field validation for 'FirstName' failed on the 'requiredtogether' tag
Field: LastName, Error: Key: 'User.LastName' Error:Field validation for 'LastName' failed on the 'requiredtogether' tag
自定义验证函数
自定义验证函数允许我们定义更复杂的字段级别验证规则。例如,验证一个字符串是否是合法的 UUID 格式。
代码示例:字段级别自定义验证
func isUUID(fl validator.FieldLevel) bool {
value := fl.Field().String()
return len(value) == 36 // 简单模拟:UUID 长度为 36 个字符
}
func main() {
validate := validator.New()
// 注册自定义验证函数
validate.RegisterValidation("uuid", isUUID)
type Resource struct {
ID string `v:"required,uuid"`
}
resource := &Resource{
ID: "invalid-uuid",
}
err := validate.Struct(resource)
if err != nil {
if validationErrors, ok := err.(validator.ValidationErrors); ok {
for _, fieldErr := range validationErrors {
fmt.Printf("Field: %s, Error: %s\n", fieldErr.Field(), fieldErr.Error())
}
} else {
fmt.Println("Validation failed with unknown error:", err)
}
}
}
输出结果
Field: ID, Error: Key: 'Resource.ID' Error:Field validation for 'ID' failed on the 'uuid' tag
表格汇总
功能 | 用法示例 | 说明 |
---|---|---|
自定义字段名称 | validate.RegisterTagNameFunc() | 从标签中提取更友好的字段名,如 fld:"First Name" |
结构体级别验证 | validate.RegisterStructValidation(func, StructType) | 在结构体级别实现复杂跨字段逻辑 |
自定义字段验证 | validate.RegisterValidation("tag", func) | 自定义验证规则(如检查 UUID 格式) |
错误报告 | sl.ReportError(field, name, structField, tag, param) | 在结构体验证中手动标记验证错误 |
补充
- errors.As()的错误处理
- ReportError进一步理解
struct_level.go
package validate
import (
"errors"
"fmt"
"github.com/go-playground/validator/v10"
"reflect"
"strings"
)
type StructLevelUser struct {
FirstName string
LastName string
Age uint8 `v:"gte=0,lte=130"`
Email string `v:"required,email" fld:"e-email"`
}
func StructLevel() {
// 注册一个自定义的标签名称函数,决定了如何从结构体字段的标签中提取验证规则和显示名称。
// 接受一个函数作为参数,该函数接收一个 reflect.StructField 类型的字段,
// 并返回一个字符串,表示该字段的标签名称。
validate.RegisterTagNameFunc(func(field reflect.StructField) string {
name := strings.SplitN(field.Tag.Get("fld"), ",", 2)[0]
/*
如果 fld:"e-email,some-other-data",strings.SplitN
会将其分为两部分:e-email 和 some-other-data,
取第一部分即 e-email
name == "-" 的意义
Go 的惯例中,标签值 "-" 通常用来表示“忽略”某个字段。例如:
在 JSON 序列化时,json:"-" 表示跳过该字段。
在这里,fld:"-" 也可以被约定为“忽略此字段的验证显示名称”。
*/
if name == "-" {
return ""
}
return name
})
// 注册一个自定义的结构体级别的验证函数。
// 它允许你在整个结构体层面进行更复杂的验证逻辑,而不仅仅是单个字段的验证。
// 接受一个函数作为参数,该函数接收一个 validator.StructLevel 类型的参数。
// StructLevel 提供了访问当前结构体实例的方法,以及报告错误的功能。
validate.RegisterStructValidation(func(sl validator.StructLevel) {
user := sl.Current().Interface().(StructLevelUser)
if len(user.LastName) == 0 && len(user.FirstName) == 0 { // 如果 FirstName 和 LastName 至少要求一个非空
// sl.ReportError 是 validator.StructLevel 提供的一个方法,用于在结构体级别的自定义验证中报告错误。
/*
fieldName
验证错误时字段的自定义名称(如需要特定显示或与代码解耦)。
一般与结构体字段名保持一致。
structFieldName
用于定位字段。
应该与字段名完全匹配。
tag
无关紧要,是规定的处理标志
自定义的标签,标识错误类型。
例如,"required" 或 "fnamelname",可根据业务需求定义。
param
可选参数,表示验证规则的额外信息。
通常用于补充说明或配置自定义逻辑(这里为空字符串)
*/
sl.ReportError(user.FirstName, "fname", "FirstName", "fnamelname", "")
sl.ReportError(user.LastName, "lname", "LastName", "fnamelname", "")
}
}, StructLevelUser{})
user := &StructLevelUser{
FirstName: "n",
LastName: "c",
Age: 100,
Email: "123nick@0voice.com",
}
err := validate.Struct(user)
if err != nil {
var InvalidValidationError *validator.InvalidValidationError
if errors.As(err, &InvalidValidationError) {
fmt.Println(err)
return
} else {
for _, err := range err.(validator.ValidationErrors) {
fmt.Println(err)
}
return
}
}
}
struct.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)
}
}
fmt.Println("All validations passed!")
}
https://github.com/0voice