不能直接在goroutine里用*gin.context,因其request-scoped,handler返回后底层http.request和responsewriter可能被回收,导致panic或脏数据;应提取原始值传入goroutine,避免使用c.copy()。

为什么不能直接在 goroutine 里用 c?
因为 *gin.Context 是 request-scoped 的,底层绑定了 *http.Request 和 http.ResponseWriter。handler 函数一返回,Go 标准库就可能回收或复用这些对象。你在 goroutine 里调用 c.Param()、c.GetHeader() 甚至 c.MustGet("user_id"),轻则拿到空值或脏数据,重则 panic。
常见错误现象:
- 日志里出现
panic: reflect: call of reflect.Value.Interface on zero Value -
c.GetString("xxx")返回空字符串,但中间件明明已 set 过 - 异步任务偶尔成功、偶尔失败,无法稳定复现
正确做法:只提取你需要的原始值(字符串、数字、结构体副本),传进 goroutine。不要传 c 本身。
c.Copy() 不是万能解药
c.Copy() 确实会复制上下文字段(如 Keys、Params、Query 等),但它不复制底层 http.Request 的 body 或 headers 引用——这些仍指向原请求对象。一旦 handler 返回,body 可能已被读取或关闭,c.Copy().Request.Body 在 goroutine 中读取时会返回 io.EOF 或阻塞。
所以:c.Copy() 只适合极简场景(比如只读 c.Param("id") 或 c.Get("user_id"))。更稳妥的方式是提前解析并拷贝:
- 用
c.ShouldBindJSON(&v)提前解析请求体,把v传入 goroutine - 用
c.GetQuery("token")拿到 token 字符串,而不是依赖c.Copy()后再取 - 避免在 goroutine 中调用任何以
c.开头的方法
通知类异步任务必须自己管超时和错误
发邮件、推消息、写日志这类通知操作,失败不影响主响应,但没人兜底就等于丢任务。别指望 goroutine 自动重试或上报。
关键点:
- 所有 HTTP 调用必须带
context.WithTimeout,比如ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - 数据库操作要确保连接池配置合理(
db.SetMaxOpenConns(20)),否则 goroutine 会排队卡死 - 错误必须显式处理:
if err != nil { log.Error("notify failed", "err", err, "user_id", userID) } - 不要用
defer cancel()—— goroutine 生命周期不确定,cancel 可能过早触发
简单通知用 goroutine 就够了,别一上来就上消息队列
如果你的“通知”是单次、低频、无强一致性要求的操作(比如用户注册后发一封欢迎邮件),直接起 goroutine 完全够用,零依赖、低延迟、易调试。
只有当出现以下任一情况时,才该考虑 RabbitMQ / Kafka / Redis Stream:
- 需要保证至少一次投递(例如支付成功后发短信,绝不允许丢失)
- 通知逻辑复杂、耗时长(>10s),且需支持失败重试、延迟重发、死信处理
- 多个服务都要消费同一类事件(比如订单创建后,库存、物流、风控都要响应)
- QPS > 500,goroutine 泛滥导致 GC 压力或内存抖动
多数内部通知(如操作日志上报、管理后台消息提醒)根本不需要消息队列——加个 go func() { ... }() + 好的日志,就是最实在的方案。











