直接将同一channel传给多个goroutine会丢消息,因channel是点对点通信机制,多个goroutine竞争读取导致每条消息仅被一个goroutine消费;正确做法是用独立goroutine将消息复制分发到多个专用channel。

为什么直接把同一个 chan event 传给多个 goroutine 会丢消息
Go 的 channel 是点对点通信原语,不是广播设备。当你写 go processemail(incoming) 和 go processpagerduty(incoming),两个 goroutine 实际上在**竞争读取**同一个 channel —— 每次只有一方能拿到值,另一方阻塞或跳过。这不是 bug,是设计使然。
常见错误现象:日志里只看到 email 处理了事件,PagerDuty 完全没反应;或者两者交替收到,但永远不同时触发。
- 根本原因:没有消息复制,只有“分时共享”
- 缓冲区大小不影响结果——哪怕
make(chan event, 100),也只改变阻塞时机,不改变单条消息只能被一个接收者消费的事实 - 别试图用
reflect.Copy或闭包“克隆” channel,Go 不支持
fan-out 广播必须靠中间 goroutine 显式复制
真正的广播 = 对每个输入事件,生成 N 份副本,分别推送到 N 个独立 channel。这一步不能省,也不能交给调度器。
关键结构:
incoming := make(chan event) // 生产者唯一写入点 emailch := make(chan event, 10) // 邮件专用,带缓冲防阻塞 pdch := make(chan event, 10) // PagerDuty 专用
分发逻辑必须单独起 goroutine:
go func() {
for e := range incoming {
select {
case emailch
- 用
select+default可防止某个 consumer channel 满了拖垮整个广播流 - 不要用
emailch 直接发送——若 consumer 暂未接收,这里就永久阻塞 - 关闭 consumer channel 的责任在广播 goroutine,不是 consumer 自己
consumer goroutine 怎么写才不泄漏、不 panic
每个 consumer 必须自己管理生命周期,且不能假设 channel 一定有数据可读。
正确写法示例(以 email 处理为例):
go func() {
for e := range emailch {
sendEmail(e)
}
}()
-
for e := range emailch是安全的:channel 关闭后自动退出,不会 panic - 不要写
for { e := —— 若 channel 关闭,会 panic: “recv on closed channel” - 如果 consumer 需要提前退出(比如 context 超时),得配合
select+ctx.Done(),但此时不能依赖range - 每个 consumer 应该有自己的 error 处理路径,别让一条失败中断整条流
缓冲区大小和 fan-out 数量怎么权衡
缓冲区不是越大越好,fan-out 数量也不是越多越快——它们共同影响内存占用、延迟和背压传导速度。
- 缓冲区设为 10~100 是常见起点;若 consumer 处理耗时波动大,可适当加大,但超过 1000 就要考虑是否设计失当
- fan-out 数量建议 ≤ CPU 核心数 × 2;超过这个数,goroutine 切换开销可能抵消并行收益
- 若下游 consumer 有 IO 等待(如 HTTP 请求),可适度增加数量;若是纯计算,保持接近核心数更稳
- 最危险的组合:无缓冲 channel + 高 fan-out 数 → 任意一个 consumer 卡住,整个广播 goroutine 立即阻塞
真正难的不是写通逻辑,而是预判哪一环最先成为瓶颈——consumer 处理速度、网络抖动、buffer 溢出、context 提前取消,这些都得在广播 goroutine 里留出退路,而不是等它卡死再看日志。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











