在通过《Go语言之旅》学习 go 的并发编程时,碰到了一个有意思的练习,并产生了一个小问题。 解决问题的方案运用了一些 go 的小技巧,于我是一次积累。
原问题:等价二叉查找树
不同二叉树的叶节点上可以保存相同的值序列。例如,以下两个二叉树都保存了序列 1,1,2,3,5,8,13。
在大多数语言中,检查两个二叉树是否保存了相同序列的函数都相当复杂。 我们将使用 Go 的并发和信道来编写一个简单的解法。
本例使用了 tree 包,它定义了类型:
type Tree struct {
Left *Tree
Value int
Right *Tree
}
要求
1. 实现 Walk 函数。
2. 测试 Walk 函数。
函数 tree.New(k) 用于构造一个随机结构的已排序二叉查找树,它保存了值 k, 2k, 3k, ..., 10k。
创建一个新的信道 ch 并且对其进行步进:
go Walk(tree.New(1), ch)
然后从信道中读取并打印 10 个值。应当是数字 1, 2, 3, ..., 10。
3. 用 Walk 实现 Same 函数来检测 t1 和 t2 是否存储了相同的值。
4. 测试 Same 函数。
Same(tree.New(1), tree.New(1)) 应当返回 true,而 Same(tree.New(1), tree.New(2)) 应当返回 false。
Tree 的文档可在这里找到。
我的问题
背景
首先,通过递归实现 Walk 函数按照左中右顺序输出二叉树中的数据非常简单:
func Walk(t *tree.Tree, ch chan<- int) {
if t == nil {
return
}
Walk(t.Left, ch)
ch <- t.Value
Walk(t.Right, ch)
}
然后,按照第 2 点要求,输出前 10 个数据也非常简单:
func TestWalk(ch <-chan int) {
for i := 0; i < 10; i++ {
fmt.Printf("%v ", <-ch)
}
}
补充 main 函数:
func main() {
ch := make(chan int)
go Walk(tree.New(1), ch)
TestWalk(ch)
}
问题
但如果要求输出所有数据呢?
- 这时候我就会自然而然地想把
TestWalk函数中的数量限制去掉,转而使用range chan的方式:func TestWalk(ch <-chan int) { for v := range ch { fmt.Printf("%v ", val) } } - 如果想使用这样的方式,必须在 chan 的写入端主动关闭,才能保证这个循环不阻塞且有限。所以,需要考虑用一种方式来关闭
ch的写入端 - 我理所当然地想在
walk()函数中调用close(ch)来关闭 chan,比如:但这样会导致一个问题——每个Walk中都会func Walk(t *tree.Tree, ch chan<- int) { defer close(ch) if t == nil { return } Walk(t.Left, ch) ch <- t.Value Walk(t.Right, ch) }close(ch),所以在递归时会提前关闭这个 chan,因而导致“写入一个关闭的通道”这样的错误。迭代法便利树就没有这个烦恼了。
解决方案
好在办法总比问题多,第二天我在网上看到了一个巧妙的解决方法,也因此积累了一个 go 语言的小技巧:
- 明确需求:在把一个树遍历完后关闭通道 ch
- 遇到问题:
Walk函数递归,所以close放在里边会提前关闭 - 解决方案:放在一个非递归的函数中,把
Walk函数包装起来! - 比如这样
func main() { ch := make(chan int) go func() { defer close(ch) Walk(tree.New(1), ch) }() TestWalk(ch) }