Fan-out需由单一goroutine分发并显式关闭输入channel,否则多worker共享未缓冲inCh会导致部分goroutine永久阻塞在range上引发死锁。

fanOut 不是标准库函数,也没有“开箱即用”的分发逻辑——它必须手动实现,且关键在于谁负责关闭输出 channel、是否缓冲、是否广播或轮询。
fanOut 分发时为什么直接 go worker(inCh) 会死锁
- 多个 goroutine 共享一个未缓冲的
inCh,读取行为不可预测:可能某个 worker 把所有数据读完,其余卡在range inCh等不到新值,但又没人关 channel - 更危险的是:如果上游没关
inCh,所有 worker 都永久阻塞在接收端,触发fatal error: all goroutines are asleep - deadlock - 正确做法是由一个分发 goroutine 控制流向,避免竞争,也便于统一关闭
常见错误写法:
for i := 0; i < 3; i++ {
go worker(inCh, outCh) // 三个 goroutine 直接 range inCh
}正确结构要点:
- 用一个 goroutine 从
inCh读,再把每个值发给多个目标 channel(广播)或选一个(负载均衡) - 每个目标 channel 必须带缓冲(如
make(chan int, 10)),否则快 worker 可能因慢 worker 未消费而阻塞发送端 - 输入 channel 关闭后,分发 goroutine 必须显式关闭所有输出 channel
如何实现广播式扇出(每个 worker 收到全部数据)
适用于事件通知、日志复制等场景,不是任务分片。
立即学习“go语言免费学习笔记(深入)”;
典型实现:
func fanOutBroadcast[T any](in <-chan T, n int) []chan T {
outs := make([]chan T, n)
for i := range outs {
outs[i] = make(chan T, 1) // 缓冲至少为 1,防瞬间积压
}
<pre class='brush:php;toolbar:false;'>go func() {
defer func() {
for _, ch := range outs {
close(ch)
}
}()
for v := range in {
for _, ch := range outs {
ch <- v // 广播给所有人
}
}
}()
return outs}
注意点:
- 不要用无缓冲 channel 做广播中间层,否则任意一个
ch <- v阻塞,整个分发就卡住 - 如果某 worker 消费极慢,它的 channel 缓冲会满,后续广播将阻塞——这是设计使然,不是 bug;若需削峰,得加背压控制(如
select+default丢弃) - 返回的
[]chan T是只接收类型,调用方不能往里 send,避免误用
如何实现负载均衡式扇出(每个任务只发给一个 worker)
适用于 HTTP 请求、数据库查询等耗时不均的任务分发。
核心是让任务主动挑 channel,而不是固定轮询:
workers := make([]chan Task, 3)
for i := range workers {
workers[i] = make(chan Task, 10)
}
<p>// 分发 goroutine
go func() {
defer close(taskCh) // 假设 taskCh 是输入源
for task := range inputTasks {
idx := rand.Intn(len(workers)) // 随机选,不锁
select {
case workers[idx] <- task:
default:
// 可选:丢弃、重试、打日志
}
}
}()</p>关键差异:
-
rand.Intn比i % len(workers)更适应真实响应时间方差 - 每个
workers[i]必须有足够缓冲(比如 10),否则随机写入容易失败 - 不要依赖
len(workers)动态变化;若需扩缩容,得用sync.RWMutex保护切片读写,但会增加复杂度
扇出最难的部分不是写 goroutine,而是决定数据怎么走、谁关 channel、缓冲设多大、超时怎么介入。这些决策一旦定错,泄漏和死锁就藏在上线后的凌晨三点。


















