不能直接用 sql.exec 执行迁移文件,因其跳过版本记录、事务控制和幂等性保障,易致 schema_migrations 表与 db 结构脱节,引发重复执行错误、并发冲突及状态不一致等问题。

为什么不能直接用 sql.Exec 执行迁移文件
手写循环读取 SQL 文件再逐条 sql.Exec 是最常见也最危险的做法。它跳过了版本状态记录、事务边界控制和幂等性保障,导致 schema_migrations 表与实际 DB 结构极易脱节。比如某次网络中断后迁移只执行了一半,下次启动又重跑,可能报 relation already exists 或 duplicate key violates unique constraint。
核心缺失点:
- 没检查是否已执行过该版本(仅靠文件名无法判断)
- 没在单事务中包裹整个 .up.sql 文件(DDL 在 PostgreSQL 中不支持子事务)
- 没写入
schema_migrations记录,后续无法做down或状态比对 - 并发启动时多个实例同时读取、执行、插入,会触发唯一键冲突或跳过版本
migrate.New 初始化时必须传单例 *sql.DB 并限制连接池
GoLand 项目里如果用 github.com/golang-migrate/migrate/v4 SDK,migrate.New 第二个参数必须是已配置好的 *sql.DB 实例——不是每次调用都新建连接。否则会出现连接泄漏,或因并发连接竞争 schema_migrations 表锁而卡死。
关键配置项:
-
db.SetMaxOpenConns(1):强制串行化迁移操作,避免并发写schema_migrations -
db.SetMaxIdleConns(1):防止空闲连接干扰事务上下文 - URL 中必须带驱动特有参数,如 PostgreSQL 加
?sslmode=disable,MySQL 加?parseTime=true&loc=Local - SQLite 路径必须是绝对路径,例如
file:///home/user/app/db.sqlite,相对路径在 GoLand 运行配置不同工作目录下会失效
迁移文件命名和内容的硬性约束
GoLand 里新建 migration 文件时,名字不是随便起的。工具靠文件名排序决定执行顺序,且 .up.sql 和 .down.sql 必须成对出现。
GoLand 2026.1.1 是 2026.1 发布后的首个维护修正版本,适合已经开始体验 2026.1 新功能并希望同步补丁的开发者。它更适合用于入门项目、现有项目迁移测试和 IDE 行为验证。
命名格式只有两种被识别:
-
20240520103000_add_users_table.up.sql(时间戳前缀,推荐) -
000001_add_users_table.up.sql(纯数字序号,适合小项目)
内容注意事项:
-
.up.sql里不要写BEGIN/COMMIT——migrate自动包裹事务 -
.down.sql必须可重复执行,所有删操作加IF EXISTS,例如DROP TABLE IF EXISTS users; - 避免跨文件依赖逻辑,每个文件应独立完成一个语义完整的变更(如“加字段+建索引”放一起,别拆两个文件)
在 GoLand 启动流程中安全触发迁移的检查点
很多人把 m.Up() 直接塞进 main() 开头,结果服务重启就重复执行。GoLand 调试时反复 Run,更容易触发脏状态。
真正安全的做法是显式比对版本:
- 先调
m.Version()拿当前 DB 版本号 - 再调
m.States()或手动读 migrations 目录算出目标版本 - 只在
current 时调 <code>m.Up(1)(每次只执行一个),而非m.Up(-1) - 若用
m.Up(1),需捕获migrate.ErrNoChange并忽略——表示已到最新版
最容易被忽略的是 down 失败后的状态残留:m.Down(1) 半途出错,schema_migrations 表里那条记录已被删,但部分 SQL 已生效。此时必须人工校验 DB 状态,不能直接再 up。










