Go 的 reflect 可实现可配置深度比较器,支持忽略字段、调用 Equal 方法、浮点容差比较等;而 reflect.DeepEqual 不支持这些定制,且对函数、NaN、不可比较 map 键值会 panic。

用 Go 的 reflect 写通用深度比较函数,核心是递归遍历两个值的结构,逐字段或逐元素比对。标准库的 reflect.DeepEqual 已经做了这件事,但若需自定义行为(比如忽略某些字段、处理 NaN、支持自定义 Equal 方法、跳过未导出字段等),就得自己实现。
理解 reflect.DeepEqual 的边界与局限
它能处理大多数情况:结构体、切片、map、指针、接口、基本类型等。但它不处理:
- 函数值(直接 panic)
- 含 NaN 的 float64/float32(NaN != NaN,导致误判不等)
- map 中键或值为不可比较类型(如 slice)时,会 panic
- 无法跳过特定字段(如时间戳、UUID 等“瞬态”字段)
- 不调用用户定义的
Equal(other T) bool方法(即使存在)
手动实现可配置的深度比较器
关键思路:用 reflect.Value 获取值的种类(Kind),按类型分治处理,并支持选项控制行为。
基础骨架如下:
立即学习“go语言免费学习笔记(深入)”;
type Comparer struct {
ignoreFields map[string]bool
useEqualMethod bool
skipUnexported bool
}
func (c *Comparer) Equal(a, b interface{}) bool {
return c.equalValue(reflect.ValueOf(a), reflect.ValueOf(b))
}
func (c *Comparer) equalValue(v1, v2 reflect.Value) bool {
// 处理零值、不同 Kind、不同类型等前置检查
if v1.Kind() != v2.Kind() {
return false
}
if !v1.IsValid() || !v2.IsValid() {
return v1.IsValid() == v2.IsValid()
}
switch v1.Kind() {
case reflect.Struct:
return c.equalStruct(v1, v2)
case reflect.Slice, reflect.Array:
return c.equalSlice(v1, v2)
case reflect.Map:
return c.equalMap(v1, v2)
case reflect.Ptr:
return c.equalPtr(v1, v2)
case reflect.Interface:
return c.equalInterface(v1, v2)
case reflect.Float32, reflect.Float64:
return c.equalFloat(v1, v2)
default:
return v1.Interface() == v2.Interface()
}
}
重点处理的几个典型场景
结构体字段跳过:遍历字段前检查字段名是否在 ignoreFields 中;同时可通过 struct tag(如 json:"-" or diff:"skip")动态控制。
支持 Equal 方法:若任一值有 Equal(interface{}) bool 方法,优先调用它(需确保参数类型兼容)。
浮点数容差比较:不直接用 ==,改用 math.Abs(a-b) ,尤其对 NaN 单独判断(math.IsNaN)。
指针解引用策略:默认解引用比较(nil 指针视为相等),也可提供选项保留指针身份比较(即地址相同才等)。
实用建议与避坑点
- 永远先做
v.IsValid()检查,避免 panic(如 nil 接口、空指针解引用) - 结构体比较前,确认字段数量和顺序一致;若依赖 tag 控制,记得用
v.Type().Field(i)获取 tag - map 比较要双向检查(key 存在性 + value 相等),避免只遍历一个 map 导致漏判
- 递归调用前加深度限制(如 100 层),防止循环引用栈溢出(可配合
map[uintptr]bool记录已访问地址) - 性能敏感场景慎用 —— reflect 比直接代码慢 10~100 倍;可考虑生成代码(如 go:generate + genny 或 stringer 思路)替代运行时反射
基本上就这些。写一个够用的通用比较器不复杂,但容易忽略循环引用、NaN、方法调用一致性等细节。从 reflect.DeepEqual 源码入手读一遍,再按需扩展,是最稳妥的路径。










