前言

A Tour of Go: Equivalent Binary Tree 我们已经练习过了如何运用 channel 提供的方法来实现两个 binary tree 的比较,具体来说,我们实现一个 Walk 函数,这个函数按照节点大小顺序遍历 binary tree 的每个节点,并且在访问每个节点的过程中,顺道将节点的值放入 channel 中,并且我们还定义一个 Same 函数,它作为每个 binary tree 对应的 channel 的消费者,通过消费两个 channel 的值,来比较两个 binary tree 是等还是不等。

虽然 binary tree 在 golang 中可被建模为结构体(有哪些字段,以及字段的类型都是确定的),但是由于同一个有限序列可由不同的 binary tree 来表示,所以,是不可能通过直接递归比较两个结构体的结构和值,来比较两个 binary tree 的。而上述这种基于 channel 的方法更加本质一些,它比较的就是 binary tree 背后的那个有限序列,而非 binary tree 结构本身。

事实上我们还可以用这种方法来比较更多的东西:任何可以通过遍历其内部结构来实现等性判定的数据结构。

实例

我们首先将 tour of go 中的代码改写成更一般的形式:新的代码将主要由一个 interface, 和一个 isEqual 函数构成:

type Walkable interface {
	Walk(chan any)
}

func IsEqual(a, b Walkable) bool {
	c1 := make(chan any)
	go func() {
		a.Walk(c1)
		close(c1)
	}()

	c2 := make(chan any)
	go func() {
		b.Walk(c2)
		close(c2)
	}()

	for {
		v1, ok1 := <-c1
		v2, ok2 := <-c2
		if ok1 && ok2 {
			if v1 == v2 {
				continue
			}
			return false
		}

		if ok1 || ok2 {
			return false
		}

		return true
	}
}

那么现在,任何一个隐式实现了 Walkable 接口的类型,都可以使用 IsEqual 函数来进行比较。我们可以比较自定义的 struct, 确定类型的 slice, 任何 ordered tree, 甚至是文件、流和图片!

我们首先用上述 IsEqual 方法比较一个自定义结构体类型:Person, 它的定义如下:

type Person struct {
	Name string
	Age  int
}

我们可以假设 Name 相同并且 Age 相同就是同一个 Person, 因此,我们可以这样实现它的 Walk receiver:

func (p *Person) Walk(c chan any) {
	c <- p.Name
	c <- p.Age
}

然后可以这样对 Person 进行比较:

a := &Person{Name: "John Doe", Age: 30}
b := &Person{Name: "Jane Doe", Age: 30}
fmt.Println("a == a:", IsEqual(a, a))
fmt.Println("a == b:", IsEqual(a, b))

接下来我们比较 tree, 定义 tree 的结构如下:

type WalkableTree struct {
	origin *tree.Tree
}

其中 tree.Tree 来自 "golang.org/x/tour/tree", WalkableTree 的 Walk receiver 实现如下:

func (t *WalkableTree) Walk(c chan any) {
	if t.origin == nil {
		return
	}
	l := &WalkableTree{origin: t.origin.Left}
	r := &WalkableTree{origin: t.origin.Right}
	l.Walk(c)
	c <- t.origin.Value
	r.Walk(c)
}

这样实现的 Walk, 可以保证 binary tree 中的元素一定是按大小顺序拿出来的。

这样调用 IsEqual 对 tree 进行比较: