HTTP handler中不可直接go fn(),必须解耦为接口校验入队立即返回200与后台worker异步执行;worker需recover、context控制、显式关闭,并通过Task结构体(含ID、Ctx、Fn、Retry等)保障幂等、超时、重试与可观测性。

别在 HTTP handler 里直接 go fn() —— 这不是异步,是埋雷。真正可用的方案,是把接口和后台 worker 拆开:接口只做校验、读取、入队、立即返回;worker 池从 channel 或 Redis 消费并执行,带 recover、context 控制、显式关闭。
HTTP handler 里必须立刻返回 200
常见错误是写成 go sendEmail(r) 就完事。问题包括:r.Body 在 handler 返回后被关闭,goroutine 里再读就是空或 panic;没控制并发,流量一上来 goroutine 泛滥;panic 静默退出,日志里完全没痕迹;进程重启,所有任务丢光。
正确做法只有三步:
- 校验签名(如
X-Hub-Signature-256),防重放 -
io.ReadAll(r.Body)一次性读完,转成[]byte或结构体 - 把任务数据(ID、参数、超时时间)写入内存 channel 或 Redis,然后
w.WriteHeader(http.StatusOK)立刻返回
用带缓冲 channel + 固定 worker 池构建内存队列
适用于单机、低延迟、可容忍丢失的场景(比如内部通知、日志归档)。核心不是“快”,是“不崩”。
立即学习“go语言免费学习笔记(深入)”;
关键配置:
-
taskCh := make(chan Task, 100)—— 缓冲大小按「峰值 QPS × 平均耗时(秒)」估算,避免生产者阻塞 - worker 数量不等于
runtime.NumCPU():I/O 密集型(发邮件、调 API)设 2~10;CPU 密集型才接近核数 - 每个 worker 必须包
defer func() { recover() }(),否则一个 panic 就 kill 整个 goroutine - worker 循环必须是
for task := range taskCh,不能只一次
Task 结构体要带 context 和元信息
裸传 func() 看似简单,但无法超时、无法取消、无法重试、无法查状态。真实项目中,Task 至少含:
-
ID string:用于幂等和日志追踪,建议用uuid.NewString() -
Ctx context.Context:透传超时与取消信号 -
Fn func(context.Context) error:执行逻辑,必须接受 context -
Retry int和MaxRetry int:为后续加指数退避中间件留接口 Result chan(可选):让调用方决定是否等待结果
不要把密钥、token 等敏感字段塞进 payload,只存 ID,worker 再查 DB 或 cache 获取详情。
Shutdown 必须显式、可观察、可等待
进程退出前不 close channel、不 wait worker,会导致程序 hang 住或任务静默丢失。
标准 shutdown 流程:
- 调用
close(taskCh)停止接收新任务 - 用
sync.WaitGroup计数所有活跃 worker,每个 worker 退出前wg.Done() - 暴露
Len() int方法返回当前len(taskCh),方便监控队列积压 - 避免在 main 函数里直接
os.Exit(0),应先exec.Shutdown()等待完成
最易被忽略的是:worker 内部的 Fn 必须检查 ctx.Err() 并及时返回,否则即使 shutdown 已触发,任务仍在跑,WaitGroup 等不到它。


















