根本原因是HTTP响应返回后连接可能被复用或关闭,ch.Publish若在handler内同步调用,易因channel失效导致panic或静默丢消息;正确做法是上传后仅序列化任务入带缓冲chan,由独立goroutine异步发送并重试。

不能在 c.SaveUploadedFile 后直接调 ch.Publish 发送 RabbitMQ 消息——HTTP 响应一返回,底层 net.Conn 就可能被复用或关闭,此时若 ch 已失效,会 panic 或静默丢消息。
为什么上传成功后调 ch.Publish 会丢消息
根本原因是 Gin handler 生命周期极短:c.JSON 或 c.String 返回后,HTTP 连接随时被 http.Server 回收。而 amqp.Channel.Publish 是同步阻塞调用,一旦 channel 被关闭、连接中断或 RabbitMQ 拒绝投递,就会触发 panic: send on closed channel,或者直接丢 payload,无日志、不可观测。
常见错误包括:
-
defer ch.Close()放在 handler 末尾,但实际ch可能在响应写出前就被上层连接池回收 - 复用全局单例
*amqp.Channel并发写入——AMQP 协议不允许多 goroutine 同时写一个 channel - 没检查
ch.IsClosed()就直接 publish,导致 panic
正确做法:上传完只投递任务到带缓冲 channel
handler 唯一该做的事是序列化任务(如文件名、用户 ID、转码参数),然后非阻塞写入一个带缓冲的 chan []byte;真正的 AMQP 发送必须交给独立 goroutine 完成,且自带重试、连接恢复和错误分类处理能力。
示例结构:
// 全局声明(初始化一次)
var taskCh = make(chan []byte, 1000)
<p>// handler 中
func uploadHandler(c *gin.Context) {
file, err := c.FormFile("file")
if err != nil {
c.String(http.StatusBadRequest, "no file")
return
}
dst := "./uploads/" + file.Filename
if err := c.SaveUploadedFile(file, dst); err != nil {
c.String(http.StatusInternalServerError, "save failed")
return
}</p><pre class="brush:php;toolbar:false;">// 序列化任务(JSON or msgpack)
task := map[string]string{
"filename": file.Filename,
"path": dst,
"uid": c.GetString("uid"),
}
data, _ := json.Marshal(task)
// 非阻塞投递(缓冲满则丢弃或返回错误,不卡住 HTTP)
select {
case taskCh <p>}
</p>关键点:
- 缓冲大小要根据峰值 QPS 和平均处理耗时估算,避免 channel 阻塞 handler
- 不要用
go publishToRabbitMQ(data)—— 每次都起 goroutine 易导致 goroutine 泄漏 - 真正发送逻辑应在服务启动时就起一个固定 goroutine 消费
taskCh
消费者 goroutine 必须支持重试与优雅退出
这个 goroutine 负责从 taskCh 取任务、建立 AMQP 连接、重试失败消息、响应系统信号。它不是“一次性的”,而是长期运行的后台 worker。
要点:
- 用
for range taskCh持续消费,内部封装connectAndPublish(),失败时 sleep 后重试(指数退避) - 连接断开时主动重建
*amqp.Connection和*amqp.Channel,不要复用已关闭实例 - 监听
os.Interrupt或syscall.SIGTERM,收到信号后先 draintaskCh,再尝试 flush 最后几条消息,最后关闭 AMQP channel 和 connection - 每条消息 publish 后检查 error,对
amqp.ErrClosed或网络错误做重试,对amqp.ErrChannelClosed触发 channel 重建
别用 amqp.AutoAck: true —— 消息一送达就确认,业务出错也无法重试;也别在 publish 后忽略 error,否则丢消息毫无感知。
最易被忽略的一点:AMQP channel 不是线程安全的,也不能跨 goroutine 复用。所有 publish 必须由同一个 goroutine 执行,哪怕你用了连接池,channel 层也要严格绑定到单一 sender goroutine。











