欢迎来到涵盖Go中并发模式系列的文章,这次我谈论的是两个类似的模式Fan In 和Fan Out 。
什么是扇入模式?
Fan-In 模式包括通过复用每个接收的值将多个通道合并成一个通道,它的工作方式是将每个接收通道的消息合并成一个。

比如说。
func main() {
ch1, err := read("file1.csv")
if err != nil {
panic(fmt.Errorf("Could not read file1 %v", err))
}
ch2, err := read("file2.csv")
if err != nil {
panic(fmt.Errorf("Could not read file2 %v", err))
}
//-
exit := make(chan struct{})
chM := merge2(ch1, ch2)
go func() {
for v := range chM {
fmt.Println(v)
}
close(exit)
}()
<-exit
fmt.Println("All completed, exiting")
}
这个main 函数可能看起来很多,但让我们把它拆开。
- 我们有一个
read函数,接收一个CSV文件名并返回一个接收通道。 - 使用该
read函数读取两个文件:file1.csv和file2.csv。 - 我们用这些通道作为
merge2函数的参数,然后返回另一个通道。 - 该通道的接收值在一个goroutine中被打印出来。
- 有一个通道
exit,只用于确保匿名goroutine完成读取所有的值。 - 这个通道
exit被关闭,并触发一个优雅的关闭。
Fan-In 模式的实现是在 merge2:
func merge2(cs ...<-chan []string) <-chan []string {
chans := len(cs)
wait := make(chan struct{}, chans)
out := make(chan []string)
send := func(c <-chan []string) {
defer func() { wait <- struct{}{} }()
for n := range c {
out <- n
}
}
for _, c := range cs {
go send(c)
}
go func() {
for range wait {
chans--
if chans == 0 {
break
}
}
close(out)
}()
return out
}
这就是很酷的事情发生的地方,对于每一个收到的通道值,我们使用匿名的send 函数启动一个goroutine,所有这些都将消息发送到out 通道,该通道被返回到我们的初始调用,然后在上述通道关闭后关闭。
注意我们还在等待一个wait 通道,用于确定何时关闭out 通道,这个通道是为了接收由send 中的defer 触发的N条消息;这被用作指示通道为空,并且goroutine完成了它的工作。
另一种做类似事情的方法是使用sync.WaitGroup 类型,例如另一种做类似事情的方法是像在 merge1做类似的事情。
func merge1(cs ...<-chan []string) <-chan []string {
var wg sync.WaitGroup
out := make(chan []string)
send := func(c <-chan []string) {
for n := range c {
out <- n
}
wg.Done()
}
wg.Add(len(cs))
for _, c := range cs {
go send(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
什么是 "扇出 "模式?
Fan-Out 模式包括通过分配每个值将一个通道分解成多个通道,它的工作方式是通过返回接收来自原始通道的值的通道。

比如说。
func main() {
ch1, err := read("file1.csv")
if err != nil {
panic(fmt.Errorf("Could not read file1 %v", err))
}
//-
br1 := breakup("1", ch1)
br2 := breakup("2", ch1)
for {
if br1 == nil && br2 == nil {
break
}
select {
case _, ok := <-br1:
if !ok {
br1 = nil
}
case _, ok := <-br2:
if !ok {
br2 = nil
}
}
}
fmt.Println("All completed, exiting")
}
这个main ,有点类似于我们在Fan-Out中的做法。
- 我们有一个
read函数,接收一个CSV文件名并返回一个接收通道。 - 这个返回的通道,
ch1,是一个要被分解的通道。 - 为了做到这一点,我们使用
breakup,返回另一个通道。 - 最后,使用一个无限的
foor循环,等待我们的通道被关闭后退出。
breakup 这个程序是这样实现的。
func breakup(worker string, ch <-chan []string) chan struct{} {
chE := make(chan struct{})
go func() {
for v := range ch {
fmt.Println(worker, v)
}
close(chE)
}()
return chE
}
如果你注意到它只是把接收到的值打印出来,我们可以做一些其他的事情,比如增强或过滤这些值;我们也可以重写这个例子,在返回的通道中实际发送这些值,这样该通道的用户可以对接收到的值做一些事情。
结论
Fan-In 和 是Fan-Out Go中最常见的两种并发模式,根据你的问题,你可能会发现它们很有用,重要的是要始终牢记一种方法来确定通道何时关闭,并适当地在下游发出一个消息来表明这一点,以便他们能够做出正确的反应。