8.2. 将并发性留给调用者

以下两个 API 有什么区别?

  1. // ListDirectory returns the contents of dir.
  2. func ListDirectory(dir string) ([]string, error)
  1. // ListDirectory returns a channel over which
  2. // directory entries will be published. When the list
  3. // of entries is exhausted, the channel will be closed.
  4. func ListDirectory(dir string) chan string

首先,最明显的不同: 第一个示例将目录读入切片然后返回整个切片,如果出错则返回错误。这是同步发生的,ListDirectory 的调用者会阻塞,直到读取了所有目录条目。根据目录的大小,这可能需要很长时间,并且可能会分配大量内存来构建目录条目。

让我们看看第二个例子。 这个示例更像是 Go 语言风格,ListDirectory 返回一个通道,通过该通道传递目录条目。当通道关闭时,表明没有更多目录条目。由于在 ListDirectory 返回后发生了通道的填充,ListDirectory 可能会启动一个 goroutine 来填充通道。

注意:第二个版本实际上不必使用 Go 协程; 它可以分配一个足以保存所有目录条目而不阻塞的通道,填充通道,关闭它,然后将通道返回给调用者。但这样做不太现实,因为会消耗大量内存来缓冲通道中的所有结果。

通道版本的 ListDirectory 还有两个问题:

  • 通过使用关闭通道作为没有其他项目要处理的信号,在中途遇到了错误时, ListDirectory 无法告诉调用者通过通道返回的项目集是否完整。调用者无法区分空目录和读取目录的错误。两者都导致从 ListDirectory 返回的通道立即关闭。
  • 调用者必须持续从通道中读取,直到它被关闭,因为这是调用者知道此通道的是否停止的唯一方式。这是对 ListDirectory 使用的严重限制,即使可能已经收到了它想要的答案,调用者也必须花时间从通道中读取。就中型到大型目录的内存使用而言,它可能更有效,但这种方法并不比原始的基于切片的方法快。

以上两种实现所带来的问题的解决方案是使用回调,该回调是在执行时在每个目录条目的上下文中调用函数。

  1. func ListDirectory(dir string, fn func(string))

毫不奇怪,这就是 filepath.WalkDir 函数的工作方式。

贴士:如果你的函数启动了 goroutine,你必须为调用者提供一种明确停止 goroutine 的方法。 把异步执行函数的决定留给该函数的调用者通常会更容易些。