gorm的automigrate不能用于生产环境,因其仅安全补全表结构(建表、加字段、扩类型、加索引/外键),不删字段、不改名、不降not null、不重排顺序、不控ddl锁,且无版本记录与回滚能力,易致schema与代码脱节。

别用 db.AutoMigrate 上生产环境做表结构变更,它不记录版本、不能回滚、删不了字段、改不了名、不控锁,线上一跑就脱节。
为什么 db.AutoMigrate 不能当生产迁移用
它只做“安全补全”:建表、加字段、扩类型(如 VARCHAR(100) → VARCHAR(255))、加索引/外键;但以下操作一律跳过:
- 删字段(
Age int从 struct 里删了,DB 里字段还在) - 改字段名(
email改成contact_email,不会执行RENAME COLUMN) - 降级约束(比如把
NOT NULL去掉,它不碰) - 重排字段顺序(
AFTER id这种位置控制,它不管) - 没有
schema_migrations表,你根本不知道测试库和生产库 schema 是否一致
更危险的是:它静默跳过冲突(比如同名索引已存在),你以为同步成功了,其实 schema 和代码已经对不上。
golang-migrate CLI 初始化迁移目录的硬性规则
迁移不是“改完代码就跑”,而是从一个明确的起始版本开始。已有线上库必须先校准状态,否则直接 migrate up 会报重复建表错。
- 运行
migrate create -ext sql -dir migrations -seq init_schema,生成000001_init_schema.up.sql和.down.sql - 序号(如
000001)由工具自增,禁止手动改——多实例部署时顺序错乱会导致迁移失败 - 已有线上库:先
migrate status查当前版本,再migrate force 1把历史状态标记为已执行 -
.up.sql只放 DDL(CREATE TABLE、ADD COLUMN),不要塞INSERT初始化数据——那是 seed,得单独管 - SQL 文件里别写
USE database_name,连接 URL 已指定库,某些 MySQL 版本会因此报错
Go 程序内调用 migrate.Up() 必须防 panic
直接在 main() 里调 m.Up() 很常见,但连接失败、SQL 语法错、MySQL 锁表超时都会 panic,生产环境不能接受。
- 必须用
context.WithTimeout(ctx, 30*time.Second)包裹,例如:m.Up(context.WithTimeout(ctx, 30*time.Second), migrate.All) - 区分错误类型:
err == migrate.ErrNoChange(无新迁移)、err == migrate.ErrLocked(被其他实例抢占锁),别一概log.Fatal -
migrate.New的数据库 URL 必须从环境变量读,如os.Getenv("DB_URL"),禁止硬编码 - MySQL 上 DDL 默认不支持事务,
ALTER TABLE失败时整个.up.sql不会回滚,.down.sql也不会自动触发——得人工干预
MySQL ADD COLUMN 卡住又没报错的真实原因
这不是 bug,是 MySQL 5.6+ 在线 DDL 的典型表现:大表加字段仍可能锁表,尤其字段插在中间、或行数超百万时,migrate 会一直等,直到上下文超时。
- 优先在 SQL 脚本里显式写
AFTER位置,如:ADD COLUMN status TINYINT DEFAULT 0 AFTER id - 确认 MySQL 版本是否支持
ALGORITHM=INPLACE:5.6 仅部分支持,5.7+ 更完善,8.0 对ADD COLUMN基本无锁 -
migrate默认不传ALGORITHM和LOCK参数,得手写进 SQL:ALTER TABLE users ADD COLUMN status TINYINT DEFAULT 0, ALGORITHM=INPLACE, LOCK=NONE; - 本地小表测通 ≠ 线上能过——预发环境必须用真实数据量压测单条迁移耗时
最常被忽略的一点:schema_migrations 表本身是迁移成功的唯一依据,但它不校验 SQL 执行结果是否符合预期;哪怕某条 ADD COLUMN 因权限不足静默跳过,只要事务提交了,版本号就进了表里。











