最稳方案是用golang-migrate cli+sql文件,禁用gorm automigrate;因其不记录历史、无法回滚、字段重命名会丢数据,且不支持多环境版本追踪与审计。

直接用 migrate CLI 做迁移,别在 Go 代码里手写 SQL 执行逻辑——后者不是演进,是埋雷。
为什么 migrate CLI 比嵌入 Go 代码更可靠
Go 生态没有内置迁移框架,migrate CLI 是事实标准:它纯靠 SQL 文件驱动,版本号硬编码在文件名里(如 000001_init_schema.up.sql),Git 能完整追踪每次变更。而自己用 database/sql 拼执行顺序,极易漏掉锁表、事务边界或 schema_migrations 表状态更新,导致线上库结构和记录脱节。
常见错误现象:
- 手动执行
CREATE TABLE后忘记插入版本记录,下次migrate up会重复建表报错 - 在 Go 代码里调
migrate.Up()时没设超时,MySQL DDL 卡住就 hang 住整个服务启动 - 多实例并发执行迁移,因未限制连接池大小,抢锁失败后状态写一半就退出
实操建议:
- 开发阶段一律用 CLI:
migrate -path ./migrations -database "mysql://root:pass@tcp(localhost:3306)/db?parseTime=true" up - 生产环境若需集成到启动流程,必须用
db.SetMaxOpenConns(1),且传context.WithTimeout(ctx, 30*time.Second) - 永远不要在
.up.sql里塞INSERT初始化数据——那是 seed,应单独管理
MySQL 上 ADD COLUMN 卡住的真正原因和解法
本地小表测试通过,上线百万行表就卡死,不是网络或权限问题,而是 MySQL 在线 DDL 的物理重排行为。5.6 支持有限,5.7+ 才真正支持 ALGORITHM=INPLACE,但 migrate 默认不启用。
实操建议:
- 写迁移 SQL 时显式声明:
ALTER TABLE users ADD COLUMN status TINYINT DEFAULT 0, ALGORITHM=INPLACE, LOCK=NONE; - 字段尽量加在末尾,避免用
AFTER xxx插入中间位置(仍可能触发重排) - 预发环境必须用真实数据量压测单条迁移耗时,不能只看本地响应
- 确认 MySQL 版本:
SELECT VERSION();,8.0 对ADD COLUMN基本无锁,5.6 则大概率锁表
已有线上库怎么安全接入 migrate
不是从头开始写迁移,而是把当前库状态“锚定”为 v1。跳过这步直接跑 migrate up,会因重复建表崩掉。
实操步骤:
- 先执行
migrate -path ./migrations -database "$DB_URL" status,看返回是否为no migrations applied - 若已有表结构,运行
migrate -path ./migrations -database "$DB_URL" force 1,把历史状态标记为已执行 - 再生成首个迁移文件:
migrate create -ext sql -dir migrations -seq init_schema,编辑.up.sql只保留CREATE TABLE IF NOT EXISTS(不带数据) - 验证:执行
migrate up应返回No change,说明锚点成功
down 迁移失败后状态卡住怎么办
migrate down 不是原子回滚,而是按序号倒序执行每个 .down.sql。一旦某条失败(比如删表前没删外键),已成功执行的步骤仍会写入 schema_migrations 表,后续 up 就跳过对应版本,导致结构错乱。
关键点:
- 开发环境可清空
schema_migrations表重试(仅限本地) - 生产环境必须补全健壮的
.down.sql:删外键优先于删主表,DROP INDEX前检查是否存在 - 永远别依赖
down做紧急修复——它只是辅助手段,核心逻辑应在.up.sql里做兼容性设计(如加字段用IF NOT EXISTS)
最常被忽略的一点:迁移脚本不是一次性的部署动作,而是长期维护的「数据库契约」。文件名里的序号、SQL 里的 ALGORITHM 参数、甚至注释格式,都会影响三年后另一个工程师的理解成本。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











