不能直接用 go func() 启动一堆 goroutine 消费消息,因为缺乏统一的生命周期控制、错误隔离和背压机制,易导致 goroutine 泄漏、panic 传播、内存暴涨或消息重复消费;需用 context.Context 控制启停、sync.WaitGroup 确保完成、带缓冲 channel 实现背压,并为每个 worker 添加独立 recover 和超时重试。

为什么不能直接用 go func() 启动一堆 goroutine 消费消息
因为缺乏统一的生命周期控制、错误隔离和背压机制,容易导致 goroutine 泄漏、panic 传播、内存暴涨或消息重复消费。比如某个消费者 panic 了,没 recover,整个程序可能崩;或者上游发得快,下游处理慢,缓冲区无限堆积。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
context.Context控制批量消费任务的启动/取消,避免 goroutine 永久挂起 - 每个消费者 goroutine 必须有独立的
recover(),防止一个出错拖垮全部 - 用带缓冲的 channel 做消息队列(如
make(chan []Message, 10)),而非无缓冲——否则生产者会阻塞 - 批量大小要可配置,别硬写死;典型值是 10–100,取决于单条消息处理耗时和下游吞吐能力
如何安全地把一批消息交给 worker 处理并等待完成
关键不是“并发执行”,而是“并发但可控完成”。推荐用 sync.WaitGroup + context.WithTimeout 组合,而不是靠 time.Sleep 或轮询。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每次从 channel 拿到
[]Message后,立刻调用wg.Add(1),再启 goroutine 处理,结束时wg.Done() - worker 内部必须检查
ctx.Err() != nil,一旦超时或取消,立即返回,不继续处理剩余消息 - 不要在 worker 里直接调用
log.Fatal或os.Exit,只返回 error,由主逻辑统一决策是否重启或告警 - 示例片段:
go func(ctx context.Context, msgs []Message) { defer wg.Done() if err := processBatch(ctx, msgs); err != nil { // 记录错误,但不 panic log.Printf("batch failed: %v", err) } }(ctx, batch)
processBatch 函数里哪些操作必须加超时和重试
网络调用(HTTP 请求、DB 查询、RPC)和外部依赖操作必须单独设超时,不能共用外层 context timeout。否则一个慢请求会拖垮整批。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 对每个外部调用,用
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)单独控制,用完记得cancel() - 重试仅针对幂等操作(如 GET /status),非幂等写操作(如 POST /order)最多重试 1 次,且需确认上游支持去重
- 避免在
processBatch中做同步日志刷盘(如os.WriteFile),改用异步日志库或缓冲写入 - 如果批量中某条消息处理失败,不要直接 return,而是记录失败索引,继续处理其余消息——提高吞吐,也方便后续补偿
如何让模块支持平滑关闭和指标暴露
生产环境最常被忽略的是 shutdown 时未等正在运行的 batch 完成,导致消息丢失;还有没人看监控,直到报警才发觉积压。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 注册
os.Interrupt和syscall.SIGTERM信号,在 shutdown handler 中先关闭输入 channel,再调wg.Wait()等所有活跃 batch 结束 - 用
expvar或prometheus.ClientGolang暴露pending_batches、processed_total、failed_total三个基础指标 - 每批处理前后打结构化日志(含 traceID、batch_size、duration_ms),别只打 “start”/“done”
- 别把 consumer 的 restart 逻辑写死在 main 里,应作为独立组件,通过健康检查接口(如
/healthz)暴露状态
真正难的不是并发模型,而是怎么让每一批都可追溯、可中断、可计量。上线前务必用真实积压场景压测 shutdown 耗时和消息零丢失。


















