13.3. 示例: 深度相等判断

来自reflect包的DeepEqual函数可以对两个值进行深度相等判断。DeepEqual函数使用内建的==比较操作符对基础类型进行相等判断,对于复合类型则递归该变量的每个基础类型然后做类似的比较判断。因为它可以工作在任意的类型上,甚至对于一些不支持==操作运算符的类型也可以工作,因此在一些测试代码中广泛地使用该函数。比如下面的代码是用DeepEqual函数比较两个字符串数组是否相等。

  1. func TestSplit(t *testing.T) {
  2. got := strings.Split("a:b:c", ":")
  3. want := []string{"a", "b", "c"};
  4. if !reflect.DeepEqual(got, want) { /* ... */ }
  5. }

尽管DeepEqual函数很方便,而且可以支持任意的数据类型,但是它也有不足之处。例如,它将一个nil值的map和非nil值但是空的map视作不相等,同样nil值的slice 和非nil但是空的slice也视作不相等。

  1. var a, b []string = nil, []string{}
  2. fmt.Println(reflect.DeepEqual(a, b)) // "false"
  3. var c, d map[string]int = nil, make(map[string]int)
  4. fmt.Println(reflect.DeepEqual(c, d)) // "false"

我们希望在这里实现一个自己的Equal函数,用于比较类型的值。和DeepEqual函数类似的地方是它也是基于slice和map的每个元素进行递归比较,不同之处是它将nil值的slice(map类似)和非nil值但是空的slice视作相等的值。基础部分的比较可以基于reflect包完成,和12.3章的Display函数的实现方法类似。同样,我们也定义了一个内部函数equal,用于内部的递归比较。读者目前不用关心seen参数的具体含义。对于每一对需要比较的x和y,equal函数首先检测它们是否都有效(或都无效),然后检测它们是否是相同的类型。剩下的部分是一个巨大的switch分支,用于相同基础类型的元素比较。因为页面空间的限制,我们省略了一些相似的分支。

gopl.io/ch13/equal

  1. func equal(x, y reflect.Value, seen map[comparison]bool) bool {
  2. if !x.IsValid() || !y.IsValid() {
  3. return x.IsValid() == y.IsValid()
  4. }
  5. if x.Type() != y.Type() {
  6. return false
  7. }
  8. // ...cycle check omitted (shown later)...
  9. switch x.Kind() {
  10. case reflect.Bool:
  11. return x.Bool() == y.Bool()
  12. case reflect.String:
  13. return x.String() == y.String()
  14. // ...numeric cases omitted for brevity...
  15. case reflect.Chan, reflect.UnsafePointer, reflect.Func:
  16. return x.Pointer() == y.Pointer()
  17. case reflect.Ptr, reflect.Interface:
  18. return equal(x.Elem(), y.Elem(), seen)
  19. case reflect.Array, reflect.Slice:
  20. if x.Len() != y.Len() {
  21. return false
  22. }
  23. for i := 0; i < x.Len(); i++ {
  24. if !equal(x.Index(i), y.Index(i), seen) {
  25. return false
  26. }
  27. }
  28. return true
  29. // ...struct and map cases omitted for brevity...
  30. }
  31. panic("unreachable")
  32. }

和前面的建议一样,我们并不公开reflect包相关的接口,所以导出的函数需要在内部自己将变量转为reflect.Value类型。

  1. // Equal reports whether x and y are deeply equal.
  2. func Equal(x, y interface{}) bool {
  3. seen := make(map[comparison]bool)
  4. return equal(reflect.ValueOf(x), reflect.ValueOf(y), seen)
  5. }
  6. type comparison struct {
  7. x, y unsafe.Pointer
  8. treflect.Type
  9. }

为了确保算法对于有环的数据结构也能正常退出,我们必须记录每次已经比较的变量,从而避免进入第二次的比较。Equal函数分配了一组用于比较的结构体,包含每对比较对象的地址(unsafe.Pointer形式保存)和类型。我们要记录类型的原因是,有些不同的变量可能对应相同的地址。例如,如果x和y都是数组类型,那么x和x[0]将对应相同的地址,y和y[0]也是对应相同的地址,这可以用于区分x与y之间的比较或x[0]与y[0]之间的比较是否进行过了。

  1. // cycle check
  2. if x.CanAddr() && y.CanAddr() {
  3. xptr := unsafe.Pointer(x.UnsafeAddr())
  4. yptr := unsafe.Pointer(y.UnsafeAddr())
  5. if xptr == yptr {
  6. return true // identical references
  7. }
  8. c := comparison{xptr, yptr, x.Type()}
  9. if seen[c] {
  10. return true // already seen
  11. }
  12. seen[c] = true
  13. }

这是Equal函数用法的例子:

  1. fmt.Println(Equal([]int{1, 2, 3}, []int{1, 2, 3})) // "true"
  2. fmt.Println(Equal([]string{"foo"}, []string{"bar"})) // "false"
  3. fmt.Println(Equal([]string(nil), []string{})) // "true"
  4. fmt.Println(Equal(map[string]int(nil), map[string]int{})) // "true"

Equal函数甚至可以处理类似12.3章中导致Display陷入死循环的带有环的数据。

  1. // Circular linked lists a -> b -> a and c -> c.
  2. type link struct {
  3. value string
  4. tail *link
  5. }
  6. a, b, c := &link{value: "a"}, &link{value: "b"}, &link{value: "c"}
  7. a.tail, b.tail, c.tail = b, a, c
  8. fmt.Println(Equal(a, a)) // "true"
  9. fmt.Println(Equal(b, b)) // "true"
  10. fmt.Println(Equal(c, c)) // "true"
  11. fmt.Println(Equal(a, b)) // "false"
  12. fmt.Println(Equal(a, c)) // "false"

练习 13.1: 定义一个深比较函数,对于十亿以内的数字比较,忽略类型差异。

练习 13.2: 编写一个函数,报告其参数是否为循环数据结构。