资讯动态

数据库迁移:goose与migrate工具

发布时间:2026/8/17 3:44:15 来源:尧图企业网站定制
数据库迁移:goose与migrate工具摘要: 本篇讲解Go项目中数据库迁移工具的使用包括goose的SQL迁移文件管理与Go代码嵌入迁移、golang-migrate的嵌入式迁移方案、版本回滚与灰度发布策略分享迁移脚本中使用了新表结构导致回滚失败的踩坑经历对比goose、golang-migrate与atlas三个工具的优劣。开篇故事去年我们团队遇到一次严重的生产事故。同事手动在数据库里加了一个字段代码还没部署上去。结果线上代码找不到这个字段接口全部报错。回滚代码也没用因为数据库结构已经变了。从那以后我们立了规矩数据库结构变更必须用迁移工具管理不能手动改。迁移工具的核心价值是把数据库变更纳入版本控制每次变更都有记录能前进也能回滚。这篇讲两个最常用的Go迁移工具:goose和golang-migrate。一、goose工具:SQL迁移文件管理goose是Go生态里最流行的迁移工具支持SQL和Go两种迁移方式。SQL迁移文件goose的SQL迁移文件用注释标记up和down。-- goose Up-- 创建用户表CREATETABLEusers(id BIGSERIALPRIMARYKEY,usernameVARCHAR(50)NOTNULLUNIQUE,emailVARCHAR(100)NOTNULLUNIQUE,password_hashVARCHAR(255)NOTNULL,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP,updated_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);CREATEINDEXidx_users_emailONusers(email);-- goose Down-- 回滚:删除表DROPTABLEIFEXISTSusers;在Go代码中嵌入迁移goose v3支持把迁移文件嵌入到二进制里部署时不需要带SQL文件。packagemainimport(contextdatabase/sqlembedfmtloggithub.com/pressly/goose/v3_github.com/lib/pq)// 把migrations目录下的SQL文件嵌入到二进制中////go:embed migrations/*.sqlvarembedMigrations embed.FSfuncmain(){// 连接数据库db,err:sql.Open(postgres,hostlocalhost port5432 userpostgres dbnamemyapp sslmodedisable)iferr!nil{log.Fatal(数据库连接失败:,err)}deferdb.Close()// 设置goose使用嵌入的迁移文件goose.SetBaseFS(embedMigrations)goose.SetDialect(postgres)// Up执行所有待执行的迁移iferr:goose.UpContext(context.Background(),db,migrations);err!nil{log.Fatal(迁移失败:,err)}// 查看当前版本version,_:goose.GetDBVersion(db)fmt.Println(当前迁移版本:,version)}常用命令在代码里也能调用。// 迁移控制命令funcmigrationCommands(db*sql.DB){ctx:context.Background()goose.Up(ctx,db,migrations)// 执行所有待迁移goose.Down(ctx,db,migrations)// 回滚最后一个迁移goose.DownTo(ctx,db,migrations,3)// 回滚到指定版本goose.Reset(ctx,db,migrations)// 回滚所有再全部执行goose.UpByOne(ctx,db,migrations)// 只前进一步goose.Status(ctx,db,migrations)// 查看状态}二、golang-migrate:嵌入式迁移golang-migrate是另一个流行工具迁移文件up和down分开。-- 000001_add_posts_table.up.sqlCREATETABLEposts(id BIGSERIALPRIMARYKEY,titleVARCHAR(200)NOTNULL,contentTEXTNOTNULL,author_idBIGINTNOTNULLREFERENCESusers(id),statusVARCHAR(20)DEFAULTdraft,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);CREATEINDEXidx_posts_authorONposts(author_id);-- 000001_add_posts_table.down.sqlDROPTABLEIFEXISTSposts;在Go中使用同样支持嵌入迁移文件://go:embed migrations/*.sqlvarmigrationsFS embed.FSfuncrunMigrate(){// 从嵌入文件系统创建迁移源source,_:iofs.New(migrationsFS,migrations)m,err:migrate.NewWithSourceInstance(iofs,source,postgres://postgreslocalhost:5432/myapp?sslmodedisable,)iferr!nil{log.Fatal(创建migrate失败:,err)}deferm.Close()// 执行迁移ErrNoChange表示没有待执行的迁移iferr:m.Up();err!nilerr!migrate.ErrNoChange{log.Fatal(迁移失败:,err)}version,dirty,_:m.Version()fmt.Printf(当前版本: %d, dirty: %v\n,version,dirty)}// 版本回滚控制funcrollbackControl(m*migrate.Migrate){m.Steps(-1)// 回退一步m.Migrate(2)// 回退到指定版本m.Force(1)// 强制设置版本修复dirty状态}三、版本回滚与灰度发布策略数据库迁移最怕的是改了结构后代码出问题需要回滚。灰度发布的思路是让迁移分步执行每一步都能独立回滚。// 灰度策略:每次只前进一步不一次性全部迁移funcgrayRelease(ctx context.Context,db*sql.DB){// UpByOneContext只执行一个迁移文件出问题影响范围小err:goose.UpByOneContext(ctx,db,migrations)iferr!nil{iferrgoose.ErrNoNextVersion{fmt.Println(已经是最新版本)}else{log.Fatal(迁移失败:,err)}}// 验证迁移结果verifyMigration(db)}// 验证迁移是否成功funcverifyMigration(db*sql.DB){varexistsbooldb.QueryRow( SELECT EXISTS ( SELECT FROM information_schema.tables WHERE table_name users ) ).Scan(exists)ifexists{fmt.Println(users表创建成功)}}灰度发布的核心原则有三条。每次只执行一个迁移文件。每个迁移文件尽量只做一件事。先在预发布环境验证再上生产。扩字段这种兼容性变更可以先迁移再部署代码缩字段这种破坏性变更要先部署兼容代码再迁移。四、独家踩坑:迁移脚本中使用了新表结构导致回滚失败上个月我们加了一个新功能需要新建退款表再给老表加字段。迁移脚本的up逻辑没问题但down逻辑出了大问题。-- 问题迁移文件-- goose UpCREATETABLErefunds(id BIGSERIALPRIMARYKEY,order_idBIGINTNOTNULL,amountDECIMAL(10,2)NOTNULL,statusVARCHAR(20)DEFAULTpending,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);ALTERTABLEordersADDCOLUMNrefund_statusVARCHAR(20)DEFAULTnone;UPDATEordersSETrefund_statusnoneWHERErefund_statusISNULL;-- goose Down-- 回滚顺序错了DROPTABLErefunds;-- 这里引用了已经不存在的字段报错UPDATEordersSETrefund_statusNULL;ALTERTABLEordersDROPCOLUMNIFEXISTSrefund_status;up执行时正常但回滚时报错column refund_status does not exist。UPDATE引用了已经不存在的字段(因为先DROP了表逻辑混乱)。迁移卡在中间状态数据库标记为dirty后续迁移全部被阻塞。排查时看goose的schema_migrations表version对应的is_dirty为true说明迁移执行到一半失败了。dirty状态下goose拒绝执行任何操作。// 修复:手动清理dirty状态funcfixDirtyState(db*sql.DB){// 方案一:手动修复版本号db.Exec( UPDATE schema_migrations SET is_dirty false, version_id version_id - 1 WHERE is_dirty true )// 方案二:用goose的Force强制设置版本// goose.Force(db, previousVersion, migrations)}修复完dirty状态后重新写了迁移脚本确保down逻辑的执行顺序和up严格相反。-- 修复后的迁移文件-- goose Up-- 先建表再加字段CREATETABLErefunds(id BIGSERIALPRIMARYKEY,order_idBIGINTNOTNULL,amountDECIMAL(10,2)NOTNULL,statusVARCHAR(20)DEFAULTpending,created_atTIMESTAMPDEFAULTCURRENT_TIMESTAMP);ALTERTABLEordersADDCOLUMNrefund_statusVARCHAR(20)DEFAULTnone;-- goose Down-- 回滚顺序严格和up相反:先删字段再删表ALTERTABLEordersDROPCOLUMNIFEXISTSrefund_status;DROPTABLEIFEXISTSrefunds;经验就是down脚本的执行顺序必须和up严格相反。up里先建表再加字段down里就要先删字段再删表。每次写完迁移文件一定要在测试环境跑一遍up再跑一遍down确认两个方向都能正常执行。五、对比分析特性goosegolang-migrateatlas迁移方式SQL和Go函数纯SQL声明式(schema即代码)嵌入支持embed.FS原生iofs适配器支持嵌入回滚支持每个迁移有down每个迁移有down声明式自动计算差异多数据库支持主流数据库支持主流数据库支持主流数据库学习成本低低中社区活跃度高高中新工具事务支持默认每个迁移一个事务默认每个迁移一个事务支持goose适合大多数项目SQL和Go两种迁移方式灵活API简洁。golang-migrate更通用迁移文件格式标准适合需要跨语言协作的团队。atlas是新兴工具声明式迁移理念先进但学习成本高适合大型项目。日常项目选goose就够了。总结与预告数据库迁移工具把schema变更纳入版本控制避免手动改表导致的事故。goose用SQL文件管理迁移up和down配对执行支持嵌入二进制。灰度发布的核心是分步迁移每次只执行一个文件。down脚本的顺序必须和up严格相反写完一定要测试回滚。到这里数据库和缓存部分的内容就讲完了。从MySQL到Redis到MongoDB从CRUD到迁移工具你现在具备了Go操作各种数据存储的完整能力。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价