不能在Gin Handler里直接发邮件,因为SMTP阻塞I/O会导致响应延迟、超时及请求失败;应通过RabbitMQ异步解耦,正确设置ContentType、DeliveryMode和Body,并复用连接、分离消费者进程。

为什么不能在Gin Handler里直接发邮件
因为SMTP调用是阻塞I/O,一次发信可能耗时300ms–2s,而Gin的HTTP handler默认超时通常只有30秒;一旦并发稍高,goroutine堆积、连接池打满、响应延迟飙升,用户看到的就是504或长转圈。更糟的是,如果邮件服务临时不可用,整个HTTP请求就失败了——这违背了“订阅只需确认接收,不保证立刻送达”的业务语义。
用RabbitMQ做中间层时,amqp.Publishing 的 ContentType 和 DeliveryMode 必须设对
很多人只填 Body,结果消费者收到乱码或无法反序列化。关键参数必须显式设置:
-
ContentType设为"application/json",否则消费者用json.Unmarshal会静默失败 -
DeliveryMode设为2(即amqp.Persistent),否则RabbitMQ重启后未消费的邮件任务全丢 -
Body必须是合法JSON字节,推荐用json.Marshal后直接传入,别手拼字符串
示例片段:
err := ch.Publish(
"", // exchange
"email_queue",
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "application/json",
DeliveryMode: amqp.Persistent,
Body: []byte(`{"to":"user@example.com","template":"welcome"}`),
},
)
Gin中启动RabbitMQ连接要避开Handler内重复 Dial
每次HTTP请求都调用 amqp.Dial 会快速耗尽文件描述符,且TCP握手开销大。正确做法是应用启动时初始化一次连接和channel,并复用:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 在
main()或 init 函数里完成amqp.Dial和Channel创建,存为全局变量或注入到gin.Engine的Keys - Handler里只调用
ch.Publish,绝不新建连接 - 加一层简单重试逻辑:若
ch.Publish返回amqp.ErrClosed,说明连接断了,需重建 channel(不是重连)
连接断开是常态,但重连成本高,重用 channel + 懒重建才是实用解法。
消费者进程不能和Gin Web服务跑同一个goroutine调度器
邮件发送逻辑(比如调用SendGrid API)本身也含网络I/O和可能的重试循环,如果把它塞进Web服务进程里用 goroutine 跑,会和HTTP请求争抢P(OS线程),尤其在高并发时导致调度延迟、GC暂停变长、甚至超时。真实生产环境必须拆成独立进程:
- 写一个单独的
cmd/email-worker/main.go,只负责监听RabbitMQ队列、执行发信、处理ACK/NACK - 用 systemd 或 supervisor 管理该worker进程生命周期,支持平滑重启
- Web服务只管“收订阅请求 → 写消息 → 返回202 Accepted”,职责清晰
看似多一个进程,实则换来隔离性、可观测性和故障边界——这才是解耦的本质,不是代码里多套channel就叫解耦。

















