直接for循环起大量goroutine发邮件易致内存爆满或smtp限流,需控制并发数、复用连接、隔离错误;推荐用带缓冲channel+固定worker数(如5个)消费mailtask任务。

goroutine 发送邮件时为什么不能直接 for 循环起一堆?
因为没加控制会瞬间创建成百上千个 goroutine,容易打爆内存或触发 SMTP 服务限流(比如 Gmail 每天 500 封、SendGrid 免费版每秒 1 个连接)。更糟的是,如果 net/smtp 客户端没复用连接,每个 goroutine 都新建 TCP 连接,SMTP 服务器可能直接拒绝后续请求。
关键不是“能不能起”,而是“怎么起才稳”——得有并发数限制 + 连接复用 + 错误隔离。
用带缓冲的 channel 控制并发数最简单可靠
把待发送的邮件塞进一个 chan *MailTask,再启动固定数量的 worker goroutine 从 channel 拿任务发。channel 缓冲区大小不重要,worker 数量才是并发上限。
- 定义任务结构:
type MailTask struct { To, Subject, Body string } - 启动 5 个 worker:
for i := 0; i -
smtpClient必须是复用的*smtp.Client(调用smtp.Dial一次,复用到所有 worker) - 发送失败的任务别丢,记日志或写入重试队列(
defer func(){...}()捕获 panic,但别 recover 网络错误)
sync.WaitGroup + 匿名 goroutine 容易踩哪些坑?
常见写法:for _, task := range tasks { wg.Add(1); go func(){...}() } —— 这里 task 是循环变量,所有 goroutine 实际共享同一个地址,最终全发错人。
- 必须显式传参:
go func(t *MailTask) { ... }(task) -
wg.Add(1)要在 goroutine 外调用,否则竞态(go run -race会报) - 别在 goroutine 里直接
wg.Done()后续还操作资源(比如关闭 smtp 连接),应确保所有发送完成再wg.Wait() - 如果用了
context.WithTimeout,记得在每个 worker 里检查ctx.Err() != nil提前退出
实际发信时 smtp.SendMail 和 smtp.Client 哪个更合适?
smtp.SendMail 看似简单,但每次调用都新建 TCP 连接 + TLS 握手 + 认证,批量场景下慢且易被封。smtp.Client 才是正解:连一次,发多封,还能用 c.Reset() 清状态复用。
- 初始化:
c, err := smtp.Dial("smtp.gmail.com:587"),然后c.Auth(...) - 每个 worker 用同一个
c,发完一封调c.Reset()(不是c.Close()) - 注意
c.Mail()和c.Rcpt()返回错误要检查,失败就跳过当前任务,别影响其他 - Gmail 等服务商要求开启「应用专用密码」或 OAuth2,普通密码会认证失败,错误信息是
534 5.7.9 Application-specific password required
真正难的不是并发,而是让每个 goroutine 知道自己该发哪封、发失败了怎么留痕、连接断了要不要自动重连——这些细节漏一点,线上就收不到告警邮件。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











