扇出是“一个输入,多个 worker 并发处理”,需通过分发 goroutine 将数据复制到多个 channel 以避免竞争和死锁;扇入是“多个来源,一个汇总出口”,用 select 轮询多个 channel 实现非阻塞合并。

什么是扇出(Fan-Out):启动多个 goroutine 处理同一数据源
扇出本质是“一个输入,多个 worker 并发处理”。典型场景是读取一个 chan int,然后启动 3 个 goroutine 同时从它收数据——但必须小心:多个 goroutine 从同一个 channel 读,会竞争,且无法保证谁读到哪条数据;更常见、更安全的做法是用一个 goroutine 负责分发,把数据复制后发给多个专用 channel。
常见错误现象:fatal error: all goroutines are asleep - deadlock,往往是因为没关 channel 或 goroutine 卡在接收端等不到数据。
- 扇出不等于“直接 go f(ch) 三次”——那样三个 goroutine 共享一个未同步关闭的
ch,最后一个可能永远阻塞 - 正确做法是用一个分发 goroutine,把输入 channel 的每个值,依次或广播式发送到多个输出 channel(如
ch1,ch2,ch3) - 如果下游处理耗时差异大,建议为每个 worker 配独立 buffer(如
make(chan int, 10)),避免快 worker 被慢 worker 拖住
什么是扇入(Fan-In):合并多个 channel 输出到一个 channel
扇入是“多个来源,一个汇总出口”。最典型实现是用 select 循环从多个 channel 接收,再把结果发到一个公共 channel。但注意:它不是“自动聚合”,而是轮询式收,哪个 ready 就收哪个。
使用场景:等待 3 个 API 请求完成并收集结果;或合并日志采集器的多路输出。
立即学习“go语言免费学习笔记(深入)”;
- 别写
for range ch1 { ... } for range ch2 { ... }——这会阻塞在第一个 channel 直到它关闭,第二个根本没机会执行 - 必须用
select+case 和 <code>case 并行监听,且所有 channel 应在不再发送时被关闭 - 如果某 channel 可能永不关闭(比如长连接心跳流),需配合
donechannel 用select带超时或退出控制,否则扇入 goroutine 会泄漏
fanIn 函数怎么写才不丢数据、不卡死
很多人抄网上示例,写个循环起 goroutine 转发,却忘了“谁关 channel”这个关键责任归属。扇入函数本身不应关闭输入 channel,但必须确保输出 channel 在所有输入结束时被关闭。
参数差异:fanIn 通常接收 []chan int,但更健壮的签名是 func fanIn[T any](cs ...,用泛型和只接收 channel 类型(<code>)来约束调用方不能往里塞可发送 channel。
- 每个输入 channel 必须由其生产者负责关闭;扇入 goroutine 只负责监听和转发
- 用
sync.WaitGroup计数活跃的输入 channel 数量,每收到一个close信号就wg.Done(),最后wg.Wait()后关闭输出 channel - 别用
defaultcase 做非阻塞接收——会导致 CPU 空转,且漏数据;真正的扇入必须阻塞等待,直到所有输入结束
真实项目里扇入扇出最容易被忽略的点
不是语法不会写,而是边界没想全:channel 关闭时机、goroutine 生命周期、错误传播路径。
- 扇出 worker 出错时,是否通知主流程?建议每个 worker 发送
struct{ value int; err error }到结果 channel,而不是只传值 - 如果扇出的 goroutine 因 panic 退出,整个流程可能静默失败——加
defer func(){ if r := recover(); r != nil { /* log */ } }() - 不要在扇入/扇出链中混用无缓冲 channel:上游写满缓冲就阻塞,下游若处理慢,整条链会卡住;优先用带缓冲 channel,容量按峰值吞吐预估
扇入扇出不是并发银弹,它让调度显式化,但也把“谁关 channel”“谁处理 panic”“谁控背压”这些责任,赤裸裸地甩给了你。










