关于 Go 并发编程学习上的一次问题记录 | 青训营;

176 阅读3分钟

在通过《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) 用于构造一个随机结构的已排序二叉查找树,它保存了值 k2k3k, ..., 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,比如:
    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)
    }
    
    但这样会导致一个问题——每个Walk中都会 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)
    }