
本文详解如何在 go 中复用 smtp 连接,避免每次发信都重新拨号与认证,通过手动管理 *smtp.client 实现高性能、低开销的邮件批量发送。
本文详解如何在 go 中复用 smtp 连接,避免每次发信都重新拨号与认证,通过手动管理 *smtp.client 实现高性能、低开销的邮件批量发送。
在 Go 标准库中,net/smtp.SendMail 是一个便捷但“一次性”的封装:它内部完成拨号、认证、发送、关闭全流程,无法复用底层 TCP 连接。对于高频率、多收件人的邮件场景(如通知服务、队列化投递),频繁建立/销毁连接会造成显著性能损耗和资源浪费。真正的解决方案是绕过 SendMail,直接使用 smtp.Client 并长期持有其生命周期。
✅ 正确做法:构建持久化 SMTP 客户端
你需要显式控制连接生命周期——初始化一次连接并复用,通过 Mail() → Rcpt() → Data() 流程逐封发送,每封邮件构成独立的 SMTP 事务(transaction),互不干扰:
package main
import (
"crypto/tls"
"log"
"net/smtp"
"time"
)
type SMTPService struct {
client *smtp.Client
addr string
}
func NewSMTPService(addr string, username, password string) (*SMTPService, error) {
// 1. 拨号建立底层连接
conn, err := smtp.Dial(addr)
if err != nil {
return nil, err
}
// 2. 发送 HELO/EHLO(必须在认证前)
if err := conn.Hello("localhost"); err != nil {
conn.Close()
return nil, err
}
// 3. 启用 TLS(如需,建议始终启用)
if ok, _ := conn.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: "localhost"} // 替换为实际 SMTP 域名
if err := conn.StartTLS(config); err != nil {
conn.Close()
return nil, err
}
}
// 4. 认证(仅需一次)
auth := smtp.PlainAuth("", username, password, "localhost") // 第四参数为 SMTP 服务器域名
if err := conn.Auth(auth); err != nil {
conn.Close()
return nil, err
}
return &SMTPService{
client: conn,
addr: addr,
}, nil
}
// SendEmail 发送单封邮件(可被多次调用,复用同一 client)
func (s *SMTPService) SendEmail(from string, to []string, msg []byte) error {
// Mail FROM
if err := s.client.Mail(from); err != nil {
return err
}
// RCPT TO(支持多个收件人)
for _, recipient := range to {
if err := s.client.Rcpt(recipient); err != nil {
return err
}
}
// DATA 开始传输内容
w, err := s.client.Data()
if err != nil {
return err
}
_, err = w.Write(msg)
if err != nil {
w.Close()
return err
}
if err := w.Close(); err != nil {
return err
}
return nil
}
// Close 安全关闭连接(应在服务退出时调用)
func (s *SMTPService) Close() error {
if s.client == nil {
return nil
}
return s.client.Quit()
}
// 使用示例
func main() {
svc, err := NewSMTPService("localhost:25", "user", "pass")
if err != nil {
log.Fatal("SMTP init failed:", err)
}
defer svc.Close()
// 发送第一封
msg1 := []byte("To: alice@example.com\r\nFrom: service@domain.com\r\nSubject: Hello 1\r\n\r\nHi there!\r\n")
if err := svc.SendEmail("service@domain.com", []string{"alice@example.com"}, msg1); err != nil {
log.Printf("Send email 1 failed: %v", err)
}
// 发送第二封(复用同一连接!)
msg2 := []byte("To: bob@example.com\r\nFrom: service@domain.com\r\nSubject: Hello 2\r\n\r\nHi again!\r\n")
if err := svc.SendEmail("service@domain.com", []string{"bob@example.com"}, msg2); err != nil {
log.Printf("Send email 2 failed: %v", err)
}
}⚠️ 关键注意事项
-
连接保活:SMTP 服务器可能因空闲超时关闭连接(常见 5–30 分钟)。生产环境建议:
- 启用 NOOP 心跳(client.Noop())定期探测;
- 或结合 time.AfterFunc 实现自动重连机制;
- 并发安全:*smtp.Client 不是 goroutine 安全的。若需并发发送,请使用连接池(如 sync.Pool)或串行化写入(如通过 channel 路由到单个 sender goroutine);
- 错误恢复:任一邮件发送失败(如 Rcpt 返回 550)不应中断整个连接;但若 Mail/Data 出现协议级错误(如连接断开),应重建客户端;
- 资源清理:务必在生命周期结束时调用 client.Quit() —— 直接 Close() 可能导致服务器残留会话。
✅ 总结
smtp.SendMail 适合简单脚本或低频场景;而构建长连接的 *smtp.Client 是构建可靠邮件服务的基石。掌握 Mail/Rcpt/Data 协议流程后,你不仅能复用连接,还可灵活支持 BCC、附件流式写入、自定义头字段等高级功能。记住:连接即资源,复用即效率,控制即稳定。










