recover仅能防止单个goroutine崩溃导致进程退出,无法保障邮件批量异步发送服务的可靠性;真正可靠需错误隔离、连接复用、重试降级、状态清理,且recover必须在每个发送goroutine内部注册、及时返回、配合上下文日志与defer资源清理。

recover 不能保障邮件批量异步发送服务的可靠性,它只能防止单个 goroutine 崩溃导致整个进程退出;真正保障服务的是错误隔离、连接复用、重试降级和状态清理——recover 只是其中一环,且必须用对位置、时机和方式。
recover 必须在每个发送 goroutine 内部独立注册
主函数或 HTTP handler 里加一个 defer func() { recover() }() 对后台发邮件的 goroutine 完全无效。goroutine 是独立调度单元,panic 不跨协程传播。
- 错误写法:
go sendEmail(to),而sendEmail函数体里没包defer+recover - 正确写法:启动时就包裹一层,例如
go func(to string) { defer func() { if r := recover(); r != nil { log.Printf("panic in email goroutine %s: %v", to, r) } }(); sendEmailAsync(to) }(to) - 如果使用
errgroup启动一批 goroutine,每个子 goroutine 都要自己处理自己的 panic,不能指望外层统一兜底
recover 后必须立即返回,不能继续操作已破坏的数据结构
邮件发送过程中若因 map 并发写入、nil 切片解引用或 channel 已关闭而 panic,recover() 成功后该 goroutine 的内存状态大概率已不一致。
- 不要在
recover后继续调用close(ch)、向刚 panic 的sync.Map写入,或尝试重发同一封邮件 - 推荐模式:
if r := recover(); r != nil { log.Error("send panic", "to", to, "err", fmt.Sprintf("%v", r)); return } - 资源清理(如关闭 SMTP 连接)应放在
defer链中,而非recover分支里——避免清理逻辑本身再 panic
recover 无法替代错误分类与重试策略
SMTP 返回 550 User unknown 或 421 Too many connections 是业务错误,不是 panic;recover 根本捕获不到,强行用 panic 包裹反而掩盖真实问题。
- 真正该 panic 的场景极少:比如模板渲染时
reflect.Value.Call崩溃、JSON 序列化空指针字段等不可恢复的运行时异常 - 网络超时、认证失败、邮箱格式错误等必须走显式
error返回 +switch分类处理 - 临时错误(4xx)可延迟重试;永久错误(5xx、550)应归档并跳过,否则会卡死整个批次
recover 日志必须带上下文,否则无法定位故障点
仅记录 "panic: runtime error: index out of range" 毫无价值。批量发送场景下,你至少需要知道这封邮件的收件人、模板 ID、任务批次号。
- 在 defer 匿名函数里捕获前,把关键变量闭包进去:
to, templateID, batchID - 避免直接
r.(error).Error()—— 若 panic 是字符串,会 panic 二次;统一用fmt.Sprintf("%v", r) - 结构化日志建议用
type switch区分:case string:、case error:、default:,再打字段
最常被忽略的一点:recover 不修复 goroutine 状态,也不释放它持有的连接或锁。如果你在发信 goroutine 里用了共享的 *smtp.Client,panic 后没手动 Close(),这个连接可能一直卡在 ESTABLISHED 状态,最终耗尽连接池。所以 defer 清理比 recover 本身更关键。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











