不能直接用 mailgun-go/v5 批量发邮件,因其无并发节流、无重试、错误不分类,易触发 Mailgun 400/min 限流或静默失败;需封装限流器、结构化错误处理和异步队列,并注意 EU 域名需显式调用 SetAPIBase(mailgun.APIBaseEU),批量发送按收件人数扣配额而非请求次数。

直接用 mailgun-go/v5 在云原生微服务里发批量邮件,不是加个 client 就完事——它默认不支持并发节流、无重试策略、错误码不分类,线上一压就丢信或触发 Mailgun 的速率限制(400/minute per domain)。必须封装一层适配层。
为什么不能直接 newMailgun() 后循环 Send()
Mailgun API 对单域名有硬性速率限制(默认 400 req/min),且 mailgun-go 的 Send() 是同步阻塞调用,没内置重试或退避。你在 Kubernetes 里起 10 个 Pod 并发调用,实际请求会打穿限流阈值,返回 429 Too Many Requests 或静默失败。更糟的是,它的 error 类型是 error 接口,没法区分是网络超时、认证失败还是配额耗尽,日志里只能看到 “failed to send”,根本没法自动降级或告警。
如何封装 mailgun-go 实现可控批量投递
核心是加三件事:限流器、结构化错误处理、异步缓冲队列。不要在 HTTP handler 里直连 Mailgun。
- 用
golang.org/x/time/rate.Limiter做每域名粒度的令牌桶,速率设为rate.Every(15 * time.Second)(即 4 req/min 留余量) - 包装
mailgun-go的 error,提取http.Status和响应 body 中的message字段,映射成自定义错误类型:ErrRateLimited、ErrInvalidRecipient、ErrAuthFailed - 投递入口改用 channel + worker goroutine 模式:业务侧只往
chan *mailgun.Message发送,后台固定 2–3 个 worker 拉取并按限流器节奏调用Send() - 失败消息写入本地磁盘(如
/tmp/mailgun-failures.jsonl)或发到 Kafka,避免内存堆积;成功后才从原始队列标记完成
注意 Mailgun EU 域名和 API Base 的坑
如果你的域名注册在 EU 区(比如 mg.yourcompany.eu),必须显式设置 API base,否则请求发到 US endpoint 会 404:
立即学习“go语言免费学习笔记(深入)”;
mg := mailgun.NewMailgun("your-domain", "your-private-key")
err := mg.SetAPIBase(mailgun.APIBaseEU) // 必须!否则报错 "invalid domain"
这个配置不写在环境变量里,也不在 NewMailgun 时传参,必须单独调用 SetAPIBase()。而且它只影响后续所有请求,不能 per-request 覆盖。很多团队在 staging 用 US 域名、prod 用 EU 域名,结果上线就全挂——因为代码里漏了这个判断分支。
批量场景下别碰 mailgun-go 的 NewMessageFromReader
文档里提过 NewMessageFromReader() 支持构造带附件的复杂邮件,但它内部用 io.Copy 读取 reader,一旦 attachment 是大文件(比如 >2MB PDF),就会卡住整个 worker goroutine,且无法设置读取超时。真实批量场景中,附件应提前上传到 S3 或 MinIO,邮件正文里只放带签名的下载链接。如果真要发附件,用 message.AddFile() + os.Open(),并确保文件句柄及时 Close(),否则容器里 fd 耗尽。
最易被忽略的点:Mailgun 的 batch 发送不是“一次 API 调用发 100 封”,而是“一次调用发一封,但收件人数组可填多个”。你传 []string{"a@x.com", "b@x.com", "c@x.com"} 给 NewMessage(),它算作 1 次请求,但计为 3 封邮件配额。别误以为能靠这个绕过限流——配额是按邮件数扣的,不是按 API 调用次数。


















