Kratos项目中不能在main.go直接调用migrate.Up(),因为启动时并发初始化多个模块会导致多个goroutine同时执行迁移,引发数据库唯一约束冲突或文件锁错误;正确做法是将迁移作为独立前置步骤,通过Makefile或shell脚本先执行migrate CLI再启动服务。

为什么 Kratos 项目里不能在 main.go 里直接调 migrate.Up()
Kratos 启动时默认会并发初始化多个模块(比如 registry、config、data),如果把 migrate.Up() 塞进 data.NewData() 或 main() 开头,多个 goroutine 可能同时触发迁移,导致 PostgreSQL 报 duplicate key value violates unique constraint "schema_migrations_version_key",或 SQLite 报文件锁冲突。Kratos 本身不提供迁移执行时序控制,它只管“连上 DB”,不管“什么时候该跑 migration”。
正确做法是把迁移作为独立启动前检查步骤:用 shell 脚本或 Makefile 先跑 CLI,再启 Kratos 服务。例如:
make migrate && make run
其中 make migrate 对应:
migrate -path ./internal/data/migrations -database "postgres://user:pass%40word@localhost:5432/mydb?sslmode=disable" up
这样既解耦了迁移逻辑,又避免了 Go 运行时并发干扰。
立即学习“go语言免费学习笔记(深入)”;
migrate create 生成的文件怎么放进 Kratos 的 internal/data 目录
Kratos 推荐按领域分包,数据库迁移文件应放在 internal/data/migrations,但 migrate create 默认生成在当前目录。别手改文件名或移动后重命名——容易漏掉 .down.sql 或破坏序号连续性。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 用
-dir显式指定路径:migrate create -ext sql -dir ./internal/data/migrations -seq add_user_profile - 生成的文件名形如
000001_add_user_profile.up.sql和000001_add_user_profile.down.sql,必须成对存在 - 确保
internal/data/migrations是相对于执行migrate命令的当前工作目录(通常是项目根),不是相对于go.mod或main.go - 文件名里不能有空格、大写字母或非数字前缀;
01_init.up.sql会被忽略(前导零不被识别为整数)
Kratos 的 *sql.DB 怎么传给 migrate CLI 不报错
CLI 不接受 Go 对象,它只认 DSN 字符串。你不能把 Kratos data.NewData().db 直接塞进去。关键是要让 CLI 用和 Kratos 完全一致的连接参数,否则会出现“本地能迁、CI 失败”或“Kratos 连得上、migrate 连不上”的割裂。
实操建议:
- 把数据库配置抽到
configs/config.yaml,Kratos 用conf.Load()加载,CLI 在 CI/CD 中用 env 替换变量拼 DSN - DNS 必须 URL 编码密码里的特殊字符:
@→%40,/→%2F,否则解析失败且日志只报unknown driver "postgres" - PostgreSQL 必加
?sslmode=disable(开发/测试)或?sslmode=require(生产);MySQL 必加?parseTime=true - 驱动名严格小写:
postgres://✅,Postgres://❌
如何让 Kratos 启动时校验迁移状态但不自动执行
有些场景(比如 FaaS 或临时调试环境)需要 Kratos 知道“当前 DB 版本是否匹配代码期望”,但又不允许它擅自执行 up。这时该用 SDK 的只读能力,而非 CLI。
在 internal/data/data.go 初始化 DB 后加一段轻量检查:
func checkMigrations(db *sql.DB) error {
m, err := migrate.New("file://./internal/data/migrations", "postgres://"+os.Getenv("DB_DSN"))
if err != nil {
return err
}
defer m.Close()
v, dirty, err := m.Version()
if err != nil && !errors.Is(err, migrate.ErrNoMigration) {
return err
}
if dirty {
return fmt.Errorf("database is dirty, version %d needs manual fix", v)
}
if v != expectedVersion {
return fmt.Errorf("database version %d mismatch, expected %d", v, expectedVersion)
}
return nil
}
注意点:
-
expectedVersion应硬编码为当前代码支持的最高迁移序号(如1598765432),或从迁移文件列表动态推导 - 不要调
m.Up(),只用m.Version();它不改库,只查schema_migrations表 - 若
m.Version()返回migrate.ErrNoMigration,说明表还没建,属于首次部署,可记录日志但不 panic
真正的迁移动作永远交给外部 CLI 控制,Kratos 只做状态感知——这是多实例部署下保持一致性最不容易翻车的方式。

















