本文面向已经在使用
github.com/gookit/validatev1.x 的用户。 This guide targets users upgrading fromgithub.com/gookit/validatev1.x.
v2.0 在设计上刻意保持最小破坏:核心 API、验证器名称、tag 语义、DataFace
接口都保持不变。绝大多数项目只需要改 import 路径与执行 go get 即可完成升级;
仅当你直接调用了 Between / ValueLen 函数时才需要额外改动。
v2.0 keeps breaking changes intentionally minimal: core API, validator names, tag
semantics and the DataFace interface are all unchanged. Most projects only need
to bump the import path. Extra work is required only if your code calls
Between or ValueLen directly.
# 1. 拉取 v2 模块 / pull the v2 module
go get github.com/gookit/validate/v2
# 2. 全局替换 import 路径 / rewrite import paths
# github.com/gookit/validate -> github.com/gookit/validate/v2
# github.com/gookit/validate/... -> github.com/gookit/validate/v2/...
# 3. 整理依赖 / tidy
go mod tidy替换示例 / import example:
// v1.x
import "github.com/gookit/validate"
import "github.com/gookit/validate/locales/zhcn"
// v2.0
import "github.com/gookit/validate/v2"
import "github.com/gookit/validate/v2/locales/zhcn"包名仍是
validate,代码中validate.Struct(...)等调用无需改动,只改 import 行。 The package name is stillvalidate; only the import path changes, not call sites.
遵循 Go Modules 的语义化版本规范,v2 模块路径带 /v2 后缀。
Per Go Modules semantic import versioning, the v2 module path carries a /v2 suffix.
- go get github.com/gookit/validate
- import "github.com/gookit/validate"
+ go get github.com/gookit/validate/v2
+ import "github.com/gookit/validate/v2"Why:Go Modules 要求 v2+ 主版本在模块路径中显式带 /vN,否则无法与 v1 共存、
也无法被正确解析。
go.mod 的最低要求从 go 1.19 提升到 go 1.21。请确保你的构建环境 Go ≥ 1.21。
- go 1.19
+ go 1.21Why:使用了 1.21 起稳定的标准库与语言能力,并与 goutil 等依赖的基线对齐。
Between 由「整数边界」改为与 Gt / Lt 一致的「任意可比较值」,统一走
valueCompare,支持 int / uint / float / string。
- func Between(val any, min, max int64) bool
+ func Between(val, min, max any) bool直接调用方一般无需改动(整型边界仍可用);但小数边界的语义发生变化:
// v1.x: min/max 为 int64,2.9 会被整数截断为 2,落入 [1,2] -> true
validate.Between(2.9, 1, 2) // => true
// v2.0: 不再截断,2.9 > 2 -> false(更符合直觉)
validate.Between(2.9, 1, 2) // => false通过 tag / StringRule 使用 between:1,2 的方式不受影响。
Why:v1 把边界强制为 int64,会把浮点边界与浮点值悄悄截断为整数,导致
2.9 落入 [1,2] 这种反直觉结果。v2 与 Gt/Lt 统一为 any,按实际数值比较。
已废弃的 ValueLen(v) 在 v2.0 移除,请改用 goutil 的 reflects.Len。
- import "github.com/gookit/validate"
- n := validate.ValueLen(reflect.ValueOf(v))
+ import "github.com/gookit/goutil/reflects"
+ n := reflects.Len(reflect.ValueOf(v))Why:该函数早已标记 deprecated,其实现本就转发到 goutil;直接用 goutil 的
reflects.Len 去掉了重复封装。reflects.Len 接收 reflect.Value,返回 int。
未变更提示:v2.0 设计期曾计划调整
DataFace接口,profile 后否决——DataFace接口保持不变,自定义DataFace实现者不受影响。 Note: theDataFaceinterface is unchanged in v2.0; custom implementers are unaffected.
v1.x 对子结构体字段(struct / *struct / slice-of-struct / map-of-struct)无条件递归
收集其内部字段规则。v2.0 改为 Java @Valid 风格的「按需下探」:仅当父字段带有
validate tag(值可为空)时才级联验证其具名子结构;具名字段完全没有 validate
tag 时不再下探。该行为由 CheckSubOnParentMarked 控制,v2 默认 true。
匿名嵌入结构体(type Bar struct { Foo },字段被提升、属父结构体一部分)豁免——
始终级联,无需 tag;只有具名嵌套字段需要标记。
In v1.x a sub-struct field was always descended into to collect its inner
rules. v2.0 makes this opt-in (Java @Valid style): cascade happens only when
the parent field carries a validate tag. An empty tag (validate:"") is
enough to mark it; a named field with no validate tag is no longer
descended into. Anonymous embedded structs are exempt (they are part of the
parent and always cascade). Controlled by CheckSubOnParentMarked, default true in v2.
type Address struct {
City string `validate:"required"`
Zip string `validate:"required|minLen:3"`
}
type User struct {
Name string `validate:"required"`
// v1.x: Address 内部的 City/Zip 规则会被自动收集并校验
// v2.0: Addr 无 validate tag -> 不再下探,City/Zip 不校验
Addr Address
}迁移方法 / Migration——二选一:
-
给需要级联的无 tag 嵌套字段补一个
validatetag(推荐,按字段精确控制)。 空 tagvalidate:""即可恢复级联,无需任何规则:type User struct { Name string `validate:"required"` - Addr Address + Addr Address `validate:""` // 空 tag 即可重新开启下探 // 或者给父字段本身加规则,如 `validate:"required"`,同样会下探 }注意:匿名嵌入字段(
User { Address })同样适用——需在嵌入行加validate:""才会下探。 -
全局恢复 v1「永远级联」行为(一处改动覆盖全部类型,适合不想逐字段标注的大型工程):
validate.Config(func(o *validate.GlobalOption) { o.CheckSubOnParentMarked = false })
Why:v1 的无条件递归会在你只想校验顶层字段时,意外地把深层子结构体的规则也 一并收集执行,且无法关闭。v2 改为显式标记后下探,行为更可预期,同时保留全局逃生舱。
为 sql.NullString、money 包装类型等「带壳」的自定义类型注册一个提取器,把它的
底层值取出来后,照常走现有的校验路径(required / 数值比较 / 长度 / 字符串规则)。
对标 go-playground/validator 的 RegisterCustomTypeFunc。
package main
import (
"database/sql"
"reflect"
"github.com/gookit/validate/v2"
)
func main() {
// 注册:从 sql.NullString 提取底层字符串;无效则返回 nil(视为空)。
validate.AddCustomType(func(field reflect.Value) any {
ns := field.Interface().(sql.NullString)
if !ns.Valid {
return nil // 返回 nil 表示"空/未设置",required 将判定失败
}
return ns.String
}, sql.NullString{})
type Form struct {
Name sql.NullString `validate:"required|minLen:3"`
}
v := validate.Struct(&Form{Name: sql.NullString{Valid: true, String: "inhere"}})
_ = v.Validate() // true:提取出 "inhere" 后按 required|minLen:3 校验
// 测试 / 清理可用 ResetCustomTypes() 复位注册表。
// validate.ResetCustomTypes()
}说明:
- 签名
AddCustomType(fn CustomTypeFunc, types ...any),CustomTypeFunc func(field reflect.Value) any。 - 提取器返回
nil表示该值为「空/未设置」,required会失败。 - 按样例的精确
reflect.Type匹配,不自动解指针:传sql.NullString{}只匹配值类型; 若要同时匹配*sql.NullString,需再传入&sql.NullString{}样例。 ResetCustomTypes()清空全部注册(测试 / 清理用)。- 未注册任何自定义类型时,校验热路径零额外开销(原子门控短路)。
Struct / Map / New 的默认行为与生命周期完全不变。如果你在循环里高频校验
同类型数据,可以显式用 Factory 复用 *Validation 实例,摊销构造成本——
复用场景的 allocs 约减半。
package main
import "github.com/gookit/validate/v2"
type User struct {
Name string `validate:"required|minLen:3"`
Email string `validate:"required|email"`
}
func main() {
users := []User{ /* ... 大量同类型数据 ... */ }
f := validate.NewFactory()
for i := range users {
v := f.Struct(&users[i]) // 从池中取复用实例 + 复用类型元数据
v.Validate()
// ... 使用 v.Errors / v.SafeData() ...
v.Release() // 必须调用:Reset 后归还到池
}
}注意事项:
- 从
Factory取得的*Validation行为与validate.Struct/validate.Map完全一致。 - 必须在用完后调用
v.Release()归还实例;Release()之后不要再使用该实例。 Release()对非工厂来源的实例(v.pool == nil)是 no-op,任何时候调用都安全。- 这是 opt-in 优化,不影响既有代码——只有显式使用
Factory的调用方才会走池化路径。
go build ./...
go vet ./...
go test ./...若以上全部通过,且你没有直接调用 Between(float, ...) 或 ValueLen、也没有依赖
子结构体的「无 tag 自动级联」(见破坏性变更第 5 项),则升级完成。若你的嵌套字段
校验在升级后「不生效」了,多半是命中了第 5 项:给该嵌套字段补 validate:"",或全局
设 CheckSubOnParentMarked = false。