扇入是将多个channel输出合并为一个channel,典型错误是用select轮询未关闭channel导致死锁或数据丢失;正确做法是为每个输入channel启独立goroutine配合for range转发,并用sync.WaitGroup等待后关闭输出channel。

什么是扇入(fan-in)?它在 Go 中的典型错误写法
扇入的本质是把多个 chan int(或其他类型)的输出,合并到一个 chan int 里。新手常直接用 select 轮询所有 channel,但漏掉关闭处理或 goroutine 泄漏风险——比如没等所有 source channel 关闭就退出,导致部分数据丢失;或者用 for range 遍历未关闭的 channel,永远阻塞。
正确做法是:每个 source channel 单独起 goroutine 拉取并转发,主 goroutine 只负责从合并后的 channel 读取;所有 source goroutine 完成后,显式关闭合并 channel。
常见错误现象:fatal error: all goroutines are asleep - deadlock,或部分数据没被消费完程序就退出。
- 必须为每个输入 channel 启动独立 goroutine,不能在主 goroutine 里用
select循环监听多个未关闭 channel - 转发 goroutine 内部要用
for range,它会自动在 source channel 关闭时退出 - 合并 channel 的关闭时机由「所有 source goroutine 结束」决定,不能提前关
标准扇入实现:用 goroutine + for range + sync.WaitGroup
这是最稳妥、可读性高、能处理任意数量 channel 的方式。核心是让每个 source channel 在自己的 goroutine 中完成「读完即退」,再用 sync.WaitGroup 等待全部结束,最后关闭输出 channel。
func fanIn(chs ...<code>chan int</code>) <code>chan int</code> {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(len(chs))
<pre class="brush:php;toolbar:false;">for _, ch := range chs {
go func(c <code>chan int</code>) {
defer wg.Done()
for v := range c {
out <p>}</p>注意:go func(c chan int) 这里必须传参捕获当前 ch 值,否则闭包会共享循环变量,导致所有 goroutine 读同一个 channel(经典陷阱)。
使用场景:日志聚合、多 API 并行调用结果合并、worker pool 输出收集。
扇出(fan-out)怎么做?别混淆「启动多个 goroutine 拉同一 channel」和「复制 channel」
扇出是指从一个 channel 分发任务给多个 goroutine 处理。关键点不是“复制 channel”,而是让多个 goroutine 共享读同一个 channel —— 因为 channel 本身是并发安全的,多个 goroutine 或 <code> 它不会出错。
错误理解:试图用 chan1, chan2 := duplicate(ch) 来“复制”channel(Go 没有内置 duplicate 函数,手写容易丢数据或死锁)。
正确做法:直接把同一个 channel 传给多个 goroutine,它们各自 for range 或 select 读取。channel 的底层调度会自动负载均衡(FIFO + 随机唤醒)。
- 不需要额外同步机制,
chan int支持多 reader - 如果需要控制并发数(比如最多 3 个 worker),用带缓冲的 channel 控制 token,而不是限制 reader 数量
- 务必确保发送方在所有数据发完后
close(),否则 reader 会永久阻塞在for range
进阶:用 reflect.Select 实现动态扇入(不推荐日常用)
当输入 channel 数量在运行时才确定、且你不想用 goroutine + WaitGroup(比如嵌入式环境省 goroutine 开销),可用 reflect.Select。但它性能差、代码难懂、调试困难,只适合极特殊场景。
典型问题:每次调用 reflect.Select 都要构造 []reflect.SelectCase,且无法区分哪个 case 被选中——除非你维护索引映射,又引入额外状态。
性能影响:比 goroutine 方案慢 3–5 倍(基准测试可见),GC 压力略高;兼容性无问题,但 Go 1.21+ 仍无优化迹象。
结论:除非 profile 明确显示 goroutine 创建是瓶颈,且你能接受可维护性下降,否则不要用 reflect.Select 做扇入。
真正容易被忽略的是:扇入后 channel 的消费者是否也做了超时/取消控制。合并后的 out 如果没人读,所有转发 goroutine 会在 out 处阻塞——所以生产代码中,<code>fanIn 返回的 channel 通常要配合 context.WithTimeout 使用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











