不能靠手写sql文件硬执行,因分支合并易致顺序错乱、多分支重复加字段、无回滚逻辑致生产不敢down;migrate是事实标准,但需显式注册驱动、注意url参数解析、ddl应拆分独立文件、测试须用真实数据库版本并隔离环境。

数据库迁移为什么不能靠手写 SQL 文件硬执行
手动维护 001_init.sql、002_add_user_index.sql 这类文件,短期看着干净,实际很快会失控:分支合并时 SQL 执行顺序错乱、同一张表在不同分支里被反复加字段、回滚逻辑缺失导致生产环境不敢跑 migrate down。Go 生态里 golang-migrate/migrate 是事实标准,但它默认不帮你管「事务边界」和「上下文传递」,直接用容易踩坑。
用 migrate.New 初始化时必须指定驱动和 URL 的组合方式
常见错误是把 PostgreSQL 的连接字符串拼成 postgres://user:pass@host/db?sslmode=disable 却忘了注册驱动——migrate 不自动 import 驱动,必须显式调用 import _ "github.com/lib/pq"(PostgreSQL)或 import _ "github.com/mattn/go-sqlite3"(SQLite)。否则 migrate.New 会报 no driver for db type postgres。
-
migrate.New第一个参数是 source(如file://migrations),第二个才是 database URL - URL 中的 query 参数(如
?sslmode=disable)会被底层 driver 解析,不是 migrate 自己处理的 - SQLite 路径要用绝对路径,相对路径在不同工作目录下行为不一致,建议用
filepath.Abs("db.sqlite")拼接
迁移函数里别直接用 tx.Exec,而要用 tx.Stmt 或 tx.Query
在 func up(m *migrate.Migration) error 里写 _, err := tx.Exec("ALTER TABLE users ADD COLUMN status TEXT") 看似没问题,但 PostgreSQL 里 DDL 语句(如 ALTER TABLE)在事务中无法回滚,一旦后续步骤失败,数据库就处于半迁移状态。更稳妥的做法是把 DDL 拆到单独 migration 文件,或者用 tx.QueryRow 做校验前置:
if err := tx.QueryRow("SELECT 1 FROM pg_tables WHERE tablename = 'users'").Scan(&exists); err != nil && err != sql.ErrNoRows {
return err
}
真正需要事务保护的操作(比如数据迁移),务必用 tx.Exec;DDL 类操作优先走独立 migration 文件,避免混在同一事务里。
测试迁移时最容易忽略 migrate.WithLogger 和临时数据库隔离
本地跑 migrate.Up 测试没问题,上线后才发现某条 SQL 在旧版本 MySQL 上语法不兼容(比如 JSON_SET 在 5.7 不支持)。问题出在测试没用真实目标版本数据库,也没捕获 SQL 执行日志。解决方案是:
- 用
migrate.WithLogger包一层log.New(os.Stderr, "[migrate] ", 0),方便定位哪条 SQL 报错 - CI 中启动 Docker 临时数据库(如
mysql:5.7),迁移脚本指向它,而不是复用开发机上的 8.0 实例 - 每次测试前用
CREATE DATABASE test_migrate+USE test_migrate隔离环境,避免残留表干扰
迁移模块真正的复杂点不在语法,而在版本、权限、锁表时机这些隐性约束——它们不会在 go test 里报错,只会在凌晨三点的生产库上突然卡住。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











