revel 的 mail.send() 不能异步,因其底层直接调用 go net/smtp,在当前 http goroutine 中同步完成 tls 握手、认证和邮件发送,无回调、channel 或队列机制;超时或错误会阻塞请求并返回 500。

Revel 框架本身不内置异步邮件能力,必须通过外部队列(如 Redis、RabbitMQ)或系统级进程(如 exec)解耦发送逻辑;直接调用 mail.Send() 始终是同步阻塞的。
为什么 Revel 的 mail.Send() 不能异步
Revel 的 github.com/revel/revel/mail 包底层直接使用 Go 标准库 net/smtp,所有调用(如 mail.Send(message))都在当前 HTTP 请求 goroutine 中完成 TLS 握手、认证、SMTP DATA 传输等全过程。没有回调、channel 或 context 取消机制,也没有内置任务队列集成点。
- HTTP handler 返回前,SMTP 连接已建立并发送完毕 —— 用户必须等待整个发信流程结束
- 若 SMTP 服务器响应慢(如 163、QQ 邮箱偶发延迟),请求可能超时或拖垮 goroutine 池
- 错误如
dial tcp: i/o timeout、535 5.7.8 Authentication failed会直接 panic 或返回 500,无法重试或落库
用 Redis + Worker 实现真正异步(推荐)
这是生产环境最可控的方式:把邮件序列化为 JSON 后推入 Redis List,由独立的长驻 Worker 进程消费并调用 mail.Send()。Revel 应用只负责“投递任务”,不参与发送。
- Worker 必须和 Revel 共享同一份配置(
MAIL_HOST、MAIL_PORT、MAIL_USERNAME等),建议提取为环境变量或 YAML 文件 - 邮件结构需 JSON-safe:
to、subject、body、html字段,避免传*revel.Controller或未导出字段 - Worker 启动命令示例:
go run worker/main.go,用supervisord或systemd守护,确保崩溃后自动重启 - 不要在 Worker 内复用 Revel 的
app.Init()—— 它会重复加载路由、拦截器,纯邮件发送只需net/smtp和配置
用 exec.Command 启动后台 PHP/Go 脚本(轻量替代)
如果不想维护额外 Worker 进程,可在 Revel handler 中用 exec.Command 启动一个无交互的子进程,将邮件内容作为参数或临时文件传入。这是“伪异步”,但对低频场景(如日均
- 必须加
&后台运行,且不等待:exec.Command("sh", "-c", "php /path/to/send_mail.php ... &") - 子脚本需自行处理异常、记录日志、失败重试(例如用
curl -X POST调用内部 API 回写状态) - 禁止传递敏感信息(如密码)到命令行参数 —— 改用临时 JSON 文件,并设
0600权限,发送后立即os.Remove() - 注意 Linux
fork()资源限制:高并发下可能触发fork: cannot allocate memory,此时必须切回 Redis 方案
最容易被忽略的点
不是怎么“发出去”,而是怎么“知道没发出去”。异步后,你失去了 HTTP 响应体的错误反馈通道。必须在 Worker 或子脚本中主动上报结果 —— 例如写入 Redis Hash(mail:status:{id})、调用 Revel 提供的 Webhook 接口、或插入数据库 mail_logs 表。否则,一封因 DNS 解析失败而卡住的邮件,会在队列里静默堆积,直到你发现监控告警断了三天。











