8.2. 将并发性留给调用者
以下两个 API 有什么区别?
// ListDirectory returns the contents of dir.
func ListDirectory(dir string) ([]string, error)
// ListDirectory returns a channel over which
// directory entries will be published. When the list
// of entries is exhausted, the channel will be closed.
func ListDirectory(dir string) chan string
首先,最明显的不同: 第一个示例将目录读入切片然后返回整个切片,如果出错则返回错误。这是同步发生的,ListDirectory
的调用者会阻塞,直到读取了所有目录条目。根据目录的大小,这可能需要很长时间,并且可能会分配大量内存来构建目录条目。
让我们看看第二个例子。 这个示例更像是 Go 语言风格,ListDirectory
返回一个通道,通过该通道传递目录条目。当通道关闭时,表明没有更多目录条目。由于在 ListDirectory
返回后发生了通道的填充,ListDirectory
可能会启动一个 goroutine
来填充通道。
注意:第二个版本实际上不必使用 Go 协程; 它可以分配一个足以保存所有目录条目而不阻塞的通道,填充通道,关闭它,然后将通道返回给调用者。但这样做不太现实,因为会消耗大量内存来缓冲通道中的所有结果。
通道版本的 ListDirectory
还有两个问题:
- 通过使用关闭通道作为没有其他项目要处理的信号,在中途遇到了错误时,
ListDirectory
无法告诉调用者通过通道返回的项目集是否完整。调用者无法区分空目录和读取目录的错误。两者都导致从ListDirectory
返回的通道立即关闭。 - 调用者必须持续从通道中读取,直到它被关闭,因为这是调用者知道此通道的是否停止的唯一方式。这是对
ListDirectory
使用的严重限制,即使可能已经收到了它想要的答案,调用者也必须花时间从通道中读取。就中型到大型目录的内存使用而言,它可能更有效,但这种方法并不比原始的基于切片的方法快。
以上两种实现所带来的问题的解决方案是使用回调,该回调是在执行时在每个目录条目的上下文中调用函数。
func ListDirectory(dir string, fn func(string))
毫不奇怪,这就是 filepath.WalkDir
函数的工作方式。
贴士:如果你的函数启动了
goroutine
,你必须为调用者提供一种明确停止goroutine
的方法。 把异步执行函数的决定留给该函数的调用者通常会更容易些。