Golang发送邮件超时及SMTP限速问题:708家企业授权邮件批量发送方案咨询
解决Golang邮件发送的速率限制与超时问题
看起来你现在面临两个核心问题:SMTP服务器的每分钟30封发送限制,以及频繁新建连接导致的超时风险。结合goroutine做批量并发+速率控制是非常合适的思路,下面给你具体的优化方案:
现有代码的核心问题
- 每次发送都用
DialAndSend新建SMTP连接,频繁的TCP握手不仅效率低,还容易触发超时 - 无速率控制的连续发送会直接触发服务器限流,导致大量发送失败
- 单线程逐个发送708封邮件需要24分钟以上,效率极低
优化后的解决方案
我们通过连接复用、分批次速率控制+批内并发发送来解决这些问题:
完整优化代码
import ( "fmt" "log" "sync" "time" "gopkg.in/gomail.v2" ) const ( FROM = "foo@some.com" PASSWORD = "bar" SMTP_HOST = "smtp.google.com" SMTP_PORT = 465 rateLimitPerMinute = 30 // 服务器限制的每分钟发送量 waitInterval = time.Minute // 批次间隔时间 ) type SendEmail struct { To string Subject string Msg string } // 复用已建立的SMTP连接发送邮件,避免重复拨号 func (se *SendEmail) Send(client *gomail.Client) error { m := gomail.NewMessage() m.SetHeader("From", FROM) m.SetHeader("To", se.To) m.SetHeader("Subject", se.Subject) m.SetBody("text/html", se.Msg) return client.Send(m) } type Company struct { Email string Login string Password string } func SendNotification(companys []Company) { // 1. 建立SMTP长连接,复用连接减少超时风险 d := gomail.NewDialer(SMTP_HOST, SMTP_PORT, FROM, PASSWORD) client, err := d.Dial() if err != nil { log.Fatalf("SMTP服务器连接失败: %v", err) } defer client.Close() total := len(companys) batchSize := rateLimitPerMinute failedCompanies := make([]Company, 0) // 2. 分批次处理邮件 for i := 0; i < total; i += batchSize { end := i + batchSize if end > total { end = total } currentBatch := companys[i:end] log.Printf("开始发送第%d-%d封邮件(共%d封)...", i+1, end, len(currentBatch)) // 3. 并发发送当前批次的邮件 var wg sync.WaitGroup for _, company := range currentBatch { wg.Add(1) // 传入循环变量副本,避免goroutine捕获同一变量的问题 go func(c Company) { defer wg.Done() sendTask := SendEmail{ To: c.Email, Subject: "我司新Web应用登录信息通知", Msg: fmt.Sprintf("您的登录账号:%s,密码:%s", c.Login, c.Password), } if err := sendTask.Send(client); err != nil { log.Printf("发送给%s失败: %v", c.Email, err) failedCompanies = append(failedCompanies, c) } else { log.Printf("发送给%s成功", c.Email) } }(company) } // 等待当前批次所有邮件发送完成 wg.Wait() log.Printf("第%d-%d封邮件发送完成", i+1, end) // 非最后一批,等待一分钟再发下一批 if end < total { log.Println("等待60秒后发送下一批...") time.Sleep(waitInterval) } } // 处理发送失败的邮件,可后续重试 if len(failedCompanies) > 0 { log.Printf("共%d封邮件发送失败,列表:%v", len(failedCompanies), failedCompanies) // 这里可以添加重试逻辑,比如:SendNotification(failedCompanies) } }
关键优化点说明
- 连接复用:通过
client.Send代替DialAndSend,保持SMTP长连接,减少TCP握手次数,大幅降低超时概率 - 批次速率控制:严格按照服务器限制分批次,每批30封,批次间隔60秒,避免触发限流
- 批内并发:每批用goroutine并发发送,原本单批30封逐个发送可能需要数分钟,并发后几秒就能完成
- 错误重试:收集发送失败的企业信息,方便后续重试,避免遗漏合作方
- 循环变量安全:goroutine中传入
company的副本,避免所有goroutine共享同一循环变量的常见陷阱
额外建议
- 可以给重试逻辑添加次数限制(比如最多重试3次),每次重试间隔递增(1分钟→2分钟→5分钟),避免重复触发限流
- 增加更详细的日志,比如记录每封邮件的发送时间,方便后续排查问题
- 如果服务器允许,可适当调整批内并发数,但不要超过30,避免触发速率限制
内容的提问来源于stack exchange,提问作者Nedvokis




