You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Golang发送邮件超时及SMTP限速问题:708家企业授权邮件批量发送方案咨询

解决Golang邮件发送的速率限制与超时问题

看起来你现在面临两个核心问题:SMTP服务器的每分钟30封发送限制,以及频繁新建连接导致的超时风险。结合goroutine做批量并发+速率控制是非常合适的思路,下面给你具体的优化方案:

现有代码的核心问题

  1. 每次发送都用DialAndSend新建SMTP连接,频繁的TCP握手不仅效率低,还容易触发超时
  2. 无速率控制的连续发送会直接触发服务器限流,导致大量发送失败
  3. 单线程逐个发送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

火山引擎 最新活动