根本原因是现代邮箱禁用明文密码登录,须用应用专用密码或开启smtp服务;gmail需先开两步验证再生成16位应用密码,qq邮箱需网页端手动开启smtp并获取授权码,端口与协议须严格匹配(587-starttls/465-ssl),用户名为完整邮箱地址,密码为授权码而非登录密码。

邮件发送接口为什么总卡在 net/smtp 认证失败?
绝大多数 Gin 邮件接口出问题,不是路由或 JSON 解析错了,而是 net/smtp 连接阶段就挂了——常见报错是 535 5.7.8 Username and Password not accepted 或 dial tcp: lookup smtp.xxx.com: no such host。根本原因:Gmail、QQ 邮箱等默认关闭 SMTP 密码登录,必须用「应用专用密码」或开启 SMTP 服务;而国内不少企业邮箱(如网易企业邮)根本不支持 STARTTLS,得改用 smtps(端口 465)并禁用 TLS 协商。
实操建议:
- QQ 邮箱:登录网页版 → 设置 → 账户 → 「POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务」→ 开启 SMTP 服务 → 生成「授权码」(不是账号密码),填到
auth := smtp.PlainAuth(...)的密码参数里 - Gmail:需先在 Google 账户中开启「两步验证」,再生成「应用专用密码」,且 SMTP 地址必须用
smtp.gmail.com:587,不能用smtp.gmail.com:465 - 避免硬编码:把
smtp_host、smtp_port、smtp_user、smtp_pass全部从.env文件读取,用godotenv.Load()加载
怎么让 Gin 接口安全接收邮件参数并校验?
直接用 c.ShouldBindJSON(&req) 绑定结构体看似简单,但容易漏掉关键校验:空收件人、超长主题、HTML 注入、附件大小失控。Gin 自带的 binding 标签只能做基础非空和长度限制,对邮箱格式、URL 安全、MIME 类型无能为力。
实操建议:
- 收件人字段用
emailtag + 自定义校验函数,调用mail.ParseAddress确保格式合法,而非只靠正则 - 主题和正文字段加
max=200和max=5000限制,防止 DoS;若允许富文本,先用bluemonday库过滤 HTML,再转义输出 - 附件上传必须限定
c.MaxMultipartMemory = 8 (8MB),并在 <code>c.FormFile("attachment")后立刻检查size > 8*1024*1024,否则恶意大文件会撑爆内存
smtp.SendMail 同步阻塞太慢,怎么不卡住 Gin 请求?
smtp.SendMail 是同步调用,发一封邮件平均耗时 300–1200ms(DNS 查询 + TLS 握手 + AUTH + DATA 传输)。如果直接在 HTTP handler 里调用,用户会明显感知延迟,且并发高时 goroutine 积压,可能触发 Gin 的 WriteTimeout。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
实操建议:
- 绝不直接在 handler 里调用
smtp.SendMail;改用带缓冲的 channel 投递任务:sendCh - 启动一个独立 goroutine 消费 channel:
go func() { for task := range sendCh { sendMailSync(task) } }(),缓冲大小设为100防止突发流量压垮 - 返回给前端的响应体里必须包含唯一
task_id(如uuid.New().String()),后续用它查投递状态,而不是假装“已发送成功”
生产环境邮件队列丢了怎么办?
纯内存 channel 在进程重启、OOM Kill 或 panic 时会丢任务,而邮件属于“至少一次”语义——发不出去比发重更不可接受。临时方案(如 Redis List + worker)比无持久化强,但别过早引入 Kafka 或 RabbitMQ。
实操建议:
- 用 SQLite 做轻量队列:建表
mail_queue(id TEXT PRIMARY KEY, status TEXT, to TEXT, subject TEXT, created_at TIMESTAMP),插入用INSERT OR IGNORE,消费后UPDATE status='sent' - worker goroutine 每 5 秒轮询一次
SELECT * FROM mail_queue WHERE status='pending' ORDER BY created_at LIMIT 10,发完立即更新状态 - 关键点:SQLite 写操作必须加
PRAGMA journal_mode = WAL,否则并发写会锁表;且所有 DB 操作要包在defer tx.Rollback()+tx.Commit()里
别指望单机 SQLite 扛百万级队列,但对日均万封以下的内部通知系统,它比折腾消息中间件实在得多——真正难的不是发邮件,是确保每封都落到收件箱里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










