直接用 go sendmail() 会崩,因无节制并发导致 smtp 连接超限、服务商限频触发封禁、内存暴涨引发 oom;正确做法是用缓冲通道(如 make(chan mailtask, 100))配合固定消费者协程实现背压控制,平衡吞吐与稳定性。

为什么直接用 go sendMail() 会崩?
很多人一上来就对每个邮件起一个 goroutine:go sendMail(to, subject, body),看似并发,实则危险。SMTP 连接数、邮件服务商限频(比如 Gmail 每天 500 封)、内存暴涨(每封邮件都带附件或大模板,goroutine 堆栈+数据累积)——不出几秒就会 panic: runtime: out of memory 或被服务商封 IP。
真正可控的高并发不是“无限制开协程”,而是“有节制地调度 + 背压控制”。缓冲区通道就是最轻量、最 Go 的节流器。
make(chan MailTask, 100) 的容量怎么选?
缓冲区大小不是越大越好,也不是拍脑袋定。它本质是“内存换可控性”的权衡点:
- 太小(如
10):发信快但容易阻塞调用方,上游 HTTP 接口可能超时; - 太大(如
10000):看似吞吐高,但失败邮件堆积在 channel 里无法及时反馈,OOM 风险陡增; - 推荐起步值:
100。对应典型 SMTP 客户端(如gomail)默认Dialer.MaxIdleConns = 2,配合 3–5 个消费者 goroutine,能稳住 30–50 封/秒,且内存占用可控(每封邮件结构体约 2–5KB)。
上线后根据 len(mailChan) 监控指标动态调整——持续 >80% 满,说明消费者慢了,优先优化发送逻辑(比如复用连接、压缩附件);持续
消费者 goroutine 怎么写才不漏信、不 panic?
别只写 for task := range mailChan —— 这样 channel 关闭后循环退出,但正在处理的邮件可能失败没重试,也没日志。正确做法:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
func (s *Mailer) startWorkers() {
for i := 0; i
<p>关键点:</p>
- 用
select+ok判断 channel 是否关闭,避免 panic; -
sendWithRetry()必须包含至少 1 次重试(网络抖动常见),且重试间隔要指数退避(如time.Sleep(time.Second ); - 失败必须落地(写 DB / 发 Kafka / 推企业微信),否则监控不到积压。
附件和模板怎么避免拖慢整个通道?
大附件(>5MB)或渲染复杂 HTML 模板会在消费者 goroutine 里卡住,导致 channel 积压。解决方案不是加大 buffer,而是提前剥离耗时操作:
- 附件路径存数据库,消费者只读取本地文件(
os.Open)并流式上传,不加载进内存; - 模板渲染放在生产者侧完成(HTTP 请求收到后立刻渲染成
body string),channel 里只传纯文本/字节,避免消费者线程竞争 template cache; - 若必须动态渲染,用
sync.Pool复用html/template.Template实例,防止频繁 GC。
记住:通道里只放“可快速序列化、低内存占用”的任务描述,所有 IO 和 CPU 密集型工作,要么前置,要么异步拆解。
缓冲区通道不是银弹——它只解决调度节奏问题。真正的稳定性来自对 SMTP 协议的理解(比如 EHLO 重试、AUTH 失败码区分)、对服务商策略的适配(Gmail 的 OAuth2、腾讯企业邮的 DKIM 签名),以及失败后的可观测性(每封邮件带唯一 traceID)。这些,比 channel 容量重要得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










