在实践中,有三种常见的纯函数签名模式:
1. 简单函数
这些函数不使用通道作为参数,而是接收和返回标准的数据结构(如结构体、切片、映射等),并且可以选择性地返回一个错误。这类函数会阻塞执行,直到结果可用。
由于并发和通道处理都在函数外部,因此这种模式具有以下优势:
- 代码简单
- 测试方便
- 可以专注于逻辑和控制流,而不被并发分散注意力
然而,这种模式只适用于可以接受阻塞并且能将完整结果存储在内存中的场景。
另一方面,如果我们需要在生成结果时就开始并发使用它们,或者需要通过流水线处理来限制内存使用,则需要使用带有通道的函数。
2. 以通道作为输入参数的函数
由于通道在 Go 中是一等公民,它们可以作为参数从调用代码传入。函数可以阻塞执行,直到完成任务。在此过程中,函数可以将生成的结果发布到通道中,以便调用者获取。
需要注意的是,函数不应关闭通道,因为创建通道的是调用者,因此关闭通道的责任也应由调用者承担。
函数可以在遇到第一个错误时立即返回,或者在完成任务后返回。
这种方式的缺点在于语义问题。函数似乎在接收通道作为输入并返回错误作为输出,而实际上通道是用于输出结果的。尽管我们可以通过指定通道的方向来澄清这一点,但语义上的不明确仍然存在。
这种模式的好处在于,调用者可以在函数执行的同时并发地处理结果,从而实现并行处理并限制内存使用。
3. 返回通道作为参数的函数
为了修复语义问题,我们可以构造一个函数来创建并返回一个通道,用于输出结果。然而,这意味着函数不再是阻塞函数,而需要在后台的 goroutine 中执行任务。
这种方式的缺点在于控制流会变得复杂,此外,由于函数不能阻塞,即使只返回一个错误,也需要通过通道来传递错误信息。
虽然这种方式解决了函数语义的问题,但却影响了函数内部的可读性。因此,在选择时需要权衡控制流的简单性与函数签名的语义之间的取舍。
结论
Go 中的通道为编写可以并发运行的纯函数提供了可能性。我常常将它与结构化并发结合使用,以便通过显而易见的控制流来提高代码的可读性。
在函数中分离关注点是编写可维护代码的基础,也是重构的核心。在这样做时,大多数函数可能最终会成为简单函数。
对于那些需要组合多个简单函数并处理循环或 I/O 的高级函数,则需要使用通道。在这种情况下,可以考虑上述两种模式,并根据控制流的简单性和函数签名的语义权衡来选择适合的方案。