不能直接在HTTP handler中用goroutine启动后台任务,因会导致资源泄漏、panic未捕获、无超时与重试、request context失效等问题;应使用带context的worker pool+channel解耦任务,确保可取消、可观测、可限流。
在 HTTP handler 里直接 go someTask() 看似简单,但极易引发资源泄漏和不可控行为:HTTP 连接关闭后 goroutine 仍在运行、panic 无法捕获、缺乏超时控制、无重试机制。更危险的是,若 someTask 依赖 request context(比如读取 r.Body),此时 body 可能已被关闭或读取完毕,导致 io.ErrUnexpectedEOF 或空数据。
核心是把异步任务从 HTTP 生命周期中解耦,同时保留可取消、可观测、可限流的能力。不依赖第三方库,标准库就能实现:
context.WithTimeout 或 context.WithCancel 包裹任务,确保上游取消时任务能退出
冲的 chan Task 做任务队列,避免突发请求压垮服务select { case 响应取消信号
type Task struct {
ID string
Data interface{}
Ctx context.Context // 来自 handler 的 context,非 background
}
var taskCh = make(chan Task, 1000)
func init() {
for i := 0; i < 4; i++ { // 启动 4 个 worker
go func() {
for t := range taskCh {
select {
case <-t.Ctx.Done():
return // 上游已取消,不执行
default:
}
processTask(t)
}
}()
}
}
func enqueueTask(w http.ResponseWriter, r http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 30time.Second)
defer cancel()
task := Task{
ID: uuid.New().String(),
Data: r.URL.Query().Get("payload"),
Ctx: ctx,
}
select {
case taskCh <- task:
w.WriteHeader(http.StatusAccepted)
json.NewEncoder(w).Encode(map[string]string{"status": "queued"})
default:
http.Error(w, "task queue full", http.StatusServiceUnavailable)
}}
什么时候该用 external broker(如 Redis / NATS)
当任务需要跨进程、跨机器、持久化、延迟执行或严格顺序保障时,纯内存 channel 就不够用了。典型场景包括:
这时应把 taskCh 替换为 redis.Client.Publish 或 nats.JetStream().PublishAsync,消费端独立部署。不要在 HTTP handler 里直连 Redis 执行耗时操作——仍要先写入队列,再由后台服务拉取执行。
以下写法很危险:
go func() {
// ❌ 错误:r.Context() 在 handler 返回后失效
// ❌ 错误:没 defer recover(),panic 会让整个 worker 退出
process(r.Context(), data)
}()正确做法是:传入显式创建的子 context,并在 goroutine 内部做 recover:
go func(ctx context.Context, data interface{}) {
defer func() {
if r := recover(); r != nil {
log.Printf("panic in async task: %v", r)
}
}()
process(ctx, data)
}(ctx, data)真正难的不是启动 goroutine,而是让每个异步任务都具备生命周期感知、错误隔离和资源确定性释放——这三点漏掉任何一项,上线后都会变成深夜告警。