migrate.run() 不该用 recover 包裹,因其错误通过显式 error 返回,如文件不存在、sql 约束冲突等,均属预期业务错误;recover 仅捕获运行时 panic,强行使用会掩盖真实问题、导致数据库状态与代码脱节。

golang-migrate/migrate 本身不 panic,也不需要你手动加 recover —— 它的错误是显式返回的 error,直接检查即可。强行在迁移逻辑里塞 defer+recover 不但没用,还会掩盖真实问题,比如权限不足、SQL 语法错误、锁表超时等本该立刻暴露的失败原因。
为什么 migrate.Run() 不该用 recover 包裹
migrate.Run() 是同步阻塞调用,所有错误都通过返回值 error 传达,例如 "no such file or directory"、"pq: duplicate key value violates unique constraint"。这类错误不是 panic,recover() 根本捕获不到。
- panic 只发生在严重运行时异常(如 nil 指针解引用、切片越界),而数据库迁移失败绝大多数是业务/环境错误,属于预期路径
- 如果你真看到 migrate 过程中 panic,那说明你用了非官方驱动、打了补丁、或自己封装时误用了
panic—— 这属于代码缺陷,不该靠recover掩盖 - 加了
recover后 error 被吞掉,日志里只剩 “migration finished” 假象,线上数据库状态和代码版本实际已脱节
真正需要 recover 的地方:迁移前/后钩子里的自定义逻辑
你在 BeforeFunc 或 AfterFunc 里写了业务代码(比如清理缓存、调用外部 API、写审计日志),这些函数才可能 panic。这时必须单独加 defer+recover,且只限该函数作用域。
- 示例:迁移成功后刷新 Redis 缓存,但 client 已关闭 →
redis.Client.Do()panic - 正确写法是在钩子函数开头就注册:
defer func() { if r := recover(); r != nil { log.Printf("panic in after-hook: %v", r) } }() - 钩子函数里不要尝试“恢复”数据库状态,只做记录和告警;迁移失败本身应由
migrate的 error 返回驱动 rollback 决策
如何安全地让迁移失败触发服务降级
迁移失败 ≠ 服务不可用。关键不是防 panic,而是把 migration error 显式转为可观察、可响应的状态信号。
- 启动时调
migrate.Up(),若返回非nilerror,记录完整错误 +debug.Stack(),然后调用srv.Shutdown()主动退出,避免带脏数据上线 - 不要用
os.Exit(1),改用sync.Once控制的优雅退出,给 k8s readiness probe 留出反应时间 - 如果支持热迁移(如某些 SaaS 场景),把迁移逻辑放进 HTTP handler,并在里面加中间件级
recover—— 但这和migrate库无关,是 handler 自身防护
最易被忽略的一点:migration 脚本里的 SQL 错误(比如 DROP TABLE IF EXISTS 在只读从库执行)会直接让 migrate 返回 error,但很多人在 CI/CD 脚本里用 || true 忽略它,结果生产库卡在半途状态。recover 解决不了这种人为绕过错误检查的问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











