deep.equal 不支持自定义比较逻辑,而 cmp.equal 配合 cmp.comparer 可为特定类型注册比较函数,如 time.time 秒级比较、字符串忽略大小写等,还可通过 filterpath 精准控制字段级行为。

deep.Equal 为什么不能直接用自定义比较逻辑
deep.Equal(来自 golang.org/x/exp/maps 或第三方如 github.com/google/go-cmp/cmp)默认只做字段级逐值递归比较,不提供钩子让开发者介入某个字段的判断过程。比如两个 time.Time 字段只要纳秒级不等就判为不同,但你可能只想比到秒;或者两个 struct 的 ID 字段是 UUID 字符串,但希望忽略大小写比较——这些都无法靠 deep.Equal 原生支持。
cmp.Equal + cmp.Comparer 是标准解法
官方推荐的 github.com/google/go-cmp/cmp 提供了 cmp.Comparer,允许你为特定类型或函数签名注册自定义比较器。它会在遍历过程中自动匹配参数类型,命中即调用你的逻辑。
常见用法:
- 为
time.Time注册秒级比较:cmp.Comparer(func(x, y time.Time) bool { return x.Unix() == y.Unix() }) - 为字符串忽略大小写:
cmp.Comparer(func(x, y string) bool { return strings.EqualFold(x, y) }) - 为指针类型解引用后比较(避免空指针 panic):
cmp.Comparer(func(x, y *MyStruct) bool { if x == nil && y == nil { return true } if x == nil || y == nil { return false } return *x == *y // 或用 cmp.Equal(*x, *y, opts...) })
注意:cmp.Comparer 的函数签名必须严格匹配字段实际类型,否则不会被调用;且不能用于 interface{} 字段(需先断言)。
自定义 Equal 方法会被 cmp 忽略?不是的,但有前提
如果你在结构体上实现了 Equal(other *T) bool 方法,cmp 默认不会调用它——除非你显式启用 cmp.AllowUnexported 并配合 cmpopts.EquateMethods。
更常见也更可控的做法是:实现 Equal 方法后,在 cmp.Equal 中用 cmp.FilterPath 指定路径,再用 cmp.ResultFromDiff 或直接调用该方法:
cmp.Equal(a, b, cmp.FilterPath(func(p cmp.Path) bool {
return p.String() == "FieldA" // 只对 FieldA 启用自定义逻辑
}, cmp.Transformer("FieldA", func(v *MyType) interface{} {
return v.Equal // 不行,Transformer 不适用
}))
其实更直白的是:直接用 cmp.FilterPath + cmp.Comparer 组合,例如:
cmp.Equal(a, b,
cmp.FilterPath(func(p cmp.Path) bool {
return p.Last().String() == "CreatedAt"
}, cmp.Comparer(func(x, y time.Time) bool {
return x.Truncate(time.Second).Equal(y.Truncate(time.Second))
})),
)
嵌套结构里部分字段要跳过比较怎么办
用 cmp.Ignore 最简单,但它会完全跳过该字段(包括其子字段)。如果只想忽略某一层的某个字段,用 cmp.FilterPath 更精准:
- 忽略所有
UpdatedAt字段:cmp.FilterPath(func(p cmp.Path) bool { return p.Last().String() == "UpdatedAt" }, cmp.Ignore()) - 忽略某个嵌套路径(如
User.Profile.AvatarURL):cmp.FilterPath(func(p cmp.Path) bool { return p.String() == "User.Profile.AvatarURL" }, cmp.Ignore()) - 注意
p.String()返回的是点号分隔路径,不含包名,大小写敏感
容易踩的坑:用 cmpopts.IgnoreFields 时传入的是 struct 类型字面量(如 reflect.TypeOf(MyStruct{})),不是实例;而且它只作用于一级字段,无法处理嵌套层级。
真正麻烦的是跨 package 的未导出字段比较,或者需要运行时动态决定是否忽略——这时候得自己写 cmp.Option 实现 cmp.Option 接口,但绝大多数场景,组合 FilterPath、Comparer 和 Ignore 已经足够。别过早抽象。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











