13.3. 示例: 深度相等判断
来自 reflect 包的 DeepEqual 对两个值进行深度相等判断. DeepEqual 使用内建的 ==
操作符对基础类型进行相等判断, 对于复合类型则递归变量每个基础类型然后做类似的比较判断. 因为它工作在任意的类型上, 甚至对一些不支持 ==
操作符的类型也可以工作, 因此在一些测试代码中被广泛地使用. 比如下面的代码是用 DeepEqual 比较两个字符串数组是否等价.
func TestSplit(t *testing.T) {
got := strings.Split("a:b:c", ":")
want := []string{"a", "b", "c"};
if !reflect.DeepEqual(got, want) { /* ... */ }
}
尽管 DeepEqual 很方便, 而且可以支持任意的类型, 但是也有不足之处. 例如, 它将一个 nil map 和 非 nil 的空的 map 视作不相等, 同样 nil slice 和 非 nil 的空的 slice 也不相等.
var a, b []string = nil, []string{}
fmt.Println(reflect.DeepEqual(a, b)) // "false"
var c, d map[string]int = nil, make(map[string]int)
fmt.Println(reflect.DeepEqual(c, d)) // "false"
在这里定义一个自己的 Equal 函数用于比较人员的值. 和 DeepEqual 类似的是它也是基于 slice 和 map 的元素进行递归比较, 不同之处是它将 nil slice(map类似) 和非 nil 的空 slice 视作相等的值. 基础部分的比较可以基于反射完成, 和 12.3 章的 Display 实现方法类似. 同样, 我们顶一个一个内部函数 equal, 用于内部的递归比较. 目前不用关心 seen 参数. 对于每一对需要比较的 x 和 y, equal 函数 首先检测它们是否都有效(或都无效), 然后检测它们是否是相同的类型. 剩下的部分是一个大的 switch 分支, 用于拥有相同基础类型的比较. 因为页面空间的限制, 我们省略了一些类似的分支.
gopl.io/ch13/equal
func equal(x, y reflect.Value, seen map[comparison]bool) bool {
if !x.IsValid() || !y.IsValid() {
return x.IsValid() == y.IsValid()
}
if x.Type() != y.Type() {
return false
}
// ...cycle check omitted (shown later)...
switch x.Kind() {
case reflect.Bool:
return x.Bool() == y.Bool()
case reflect.String:
return x.String() == y.String()
// ...numeric cases omitted for brevity...
case reflect.Chan, reflect.UnsafePointer, reflect.Func:
return x.Pointer() == y.Pointer()
case reflect.Ptr, reflect.Interface:
return equal(x.Elem(), y.Elem(), seen)
case reflect.Array, reflect.Slice:
if x.Len() != y.Len() {
return false
}
for i := 0; i < x.Len(); i++ {
if !equal(x.Index(i), y.Index(i), seen) {
return false
}
}
return true
// ...struct and map cases omitted for brevity...
}
panic("unreachable")
}
和前面的建议一样, 我们不公开使用反射相关的接口, 所以导出的函数需要在内部自己将变量转为 reflect.Value 类型.
// Equal reports whether x and y are deeply equal.
func Equal(x, y interface{}) bool {
seen := make(map[comparison]bool)
return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
}
type comparison struct {
x, y unsafe.Pointer
treflect.Type
}
为了确保算法对于循环数据结构也能正常退出, 我们必须记录每次已经比较的变量, 从而避免进入第二次的比较. Equal 函数分配了一组用于比较的结构体, 包含每对比较对象的地址(unsafe.Pointer形式保存)和类型. 我们记录类型的原因是, 有些不同的变量可能对应相同的地址. 例如, 如果 x 和 y 都是数组类型, 那么 x 和 x[0]
将对应相同的地址, y 和 y[0]
也是对应相同的地址, 这可以用于判断 对x 和 y 比较 或 x[0] 和 y[0] 的是否进行过了.
// cycle check
if x.CanAddr() && y.CanAddr() {
xptr := unsafe.Pointer(x.UnsafeAddr())
yptr := unsafe.Pointer(y.UnsafeAddr())
if xptr == yptr {
return true // identical references
}
c := comparison{xptr, yptr, x.Type()}
if seen[c] {
return true // already seen
}
seen[c] = true
}
这是 Equal 函数的使用的例子:
fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true"
fmt.Println(Equal([]string{"foo"}, []string{"bar"})) // "false"
fmt.Println(Equal([]string(nil), []string{})) // "true"
fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"
它甚至可以处理类似12.3章中导致Display陷入死循环的数据.
// Circular linked lists a -> b -> a and c -> c.
type link struct {
value string
tail *link
}
a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"}
a.tail, b.tail, c.tail = b, a, c
fmt.Println(Equal(a, a)) // "true"
fmt.Println(Equal(b, b)) // "true"
fmt.Println(Equal(c, c)) // "true"
fmt.Println(Equal(a, b)) // "false"
fmt.Println(Equal(a, c)) // "false"
练习 13.1: 定义一个深比较函数, 对于十亿以内的数字比较, 忽略类型差异.
练习 13.2: 编写一个函数, 报告其参数是否循环数据结构.