gin handler中不能直接调用amqp.publish,因响应返回后连接可能被复用或关闭,导致panic或消息丢失;应通过带缓冲channel中转,由独立goroutine异步发送并重试。

gin.Handler 里不能直接调用 amqp.Publish
HTTP handler 生命周期极短,w.WriteHeader() 或 c.JSON() 返回后,底层 net.Conn 就可能被复用或关闭。此时若 amqp.Channel.Publish 正在执行(比如网络抖动、RabbitMQ 拒绝连接、channel 已 close),会 panic:send on closed channel,或静默丢消息。
这不是异步,是“扔完不管”。压测或网络波动时,任务批量丢失是常态。
- 常见错误包括:
defer ch.Close()放在 handler 末尾,但 channel 实际已在响应写出后被回收 - 没检查
ch.IsClosed(),复用已关闭的 channel 导致 panic - 用全局单例
*amqp.Channel并发写入 —— AMQP 协议不允许多 goroutine 同时写同一 channel
必须用带缓冲的 channel 中转消息
核心原则:Gin handler 只做两件事——序列化 + 投递到带缓冲的 chan []byte;真正 AMQP 发送由独立 goroutine 完成,且必须自带重试和连接恢复逻辑。
缓冲大小不是拍脑袋定的,要按峰值 QPS × 平均处理延迟预估。例如 QPS 500、平均发送耗时 200ms,缓冲至少 100。
- handler 中只做非阻塞投递:
select { case msgCh - 单独 goroutine 消费:
go func() { for payload := range msgCh { publishToRabbitMQ(payload) } }() -
publishToRabbitMQ必须每次新建amqp.Publishing实例,避免headers复用污染 - 必须捕获
amqp.Error类型(如NOT_FOUND队列不存在),不能直接 panic 或忽略
goroutine 中禁止直接使用 *gin.Context
gin.Context 是 request-scoped 对象,handler 返回后其底层 http.ResponseWriter 和 *http.Request 可能被回收或复用。在 goroutine 中调用 c.Param()、c.GetHeader()、c.MustGet() 等方法,轻则返回脏数据,重则 panic。
- 务必在启动 goroutine 前提取所需字段并传入:字符串、数字、结构体副本(深拷贝)
- 不要用
c.Copy()传给 RabbitMQ 生产者 ——c.Copy()只用于跨 goroutine 继续处理请求(如中间件链),不适用于消息队列投递场景 - 若需上下文取消能力,应从
c.Request.Context()提取context.Context,再用context.WithTimeout封装后台逻辑
ACK 失败往往是因为根本没发出去
RabbitMQ 看不到 ACK,通常不是 broker 不给,而是业务逻辑里 return、panic 或未 recover 的 error 导致 msg.Ack(false) 根本没执行。消息一直锁在 unacked 状态,积压触发流控后整个 channel 被 block,新消息进不来。
-
msg.Ack(false)必须放在defer里,且defer前确保msg非 nil - 业务逻辑必须包在
defer func() { if r := recover(); r != nil { msg.Nack(false, true) } }()中 - 禁用
msg.Ack(true)(multiple=true),它会批量确认前面所有未 ack 消息,容易误确认
最易被忽略的一点:消息体本身是否可序列化、是否含指针或闭包 —— Gin handler 里传给 goroutine 的 payload,一旦含未导出字段或 runtime 匿名函数,JSON 编码会静默跳过,导致消费者收到空数据。











