当前位置:首页 > 文章列表 > Golang > Go教程 > 在二进制中嵌入迁移文件并按版本顺序执行

在二进制中嵌入迁移文件并按版本顺序执行

来源:17golang原创 2026-10-08 21:38:30 0浏览 收藏

最稳妥的做法是:用 //go:embed migrations/*.sql 把迁移文件编译进 embed.FS,启动时解析文件名中的版本号并显式排序,只执行 schema_migrations 中尚未登记的版本。每次迁移都应把“执行 SQL”和“写入已应用版本”放在同一个 sql.Tx 中,成功后再提交。

要点速览
  • 文件名使用固定宽度版本,例如 0001_init.sql,不要修改已经发布的迁移。
  • fs.Glob 返回匹配文件,但执行器仍应显式排序并校验版本,避免把命名习惯当成唯一保障。
  • schema_migrations 是数据库端的事实来源,二进制中的文件只是可用迁移集合。
  • 多语句 SQL、DDL 事务和并发部署都与数据库及驱动相关,生产环境要明确边界。

先把迁移文件变成可排序的内置资源

目录可以保持简单:migrations/0001_init.sql、migrations/0002_add_email.sql。四位数字让字典序与版本序一致,后面的名称用于阅读和审计。Go 的 embed 会在编译期把匹配文件放进二进制,因此部署时不需要再携带一个易丢失的 SQL 目录。

migrations 目录、SQL 文件、go embed、embed.FS、版本解析器与排序后迁移清单的静态结构图
图1:迁移资源与版本索引的静态结构图,不是运行截图。
package migrate

import (
	"embed"
	"fmt"
	"io/fs"
	"path"
	"sort"
	"strconv"
	"strings"
)

// 编译期把 migrations 目录中的 SQL 文件写入二进制。
//go:embed migrations/*.sql
var migrationFS embed.FS

type Migration struct {
	Version int
	Name    string
	Path    string
}

func loadMigrations() ([]Migration, error) {
	// Glob 面向内置文件系统读取,不依赖部署机器上的工作目录。
	files, err := fs.Glob(migrationFS, "migrations/*.sql")
	if err != nil {
		return nil, fmt.Errorf("glob migrations: %w", err)
	}
	// 明确排序,使执行顺序在代码层可见。
	sort.Strings(files)

	items := make([]Migration, 0, len(files))
	seen := make(map[int]string, len(files))
	for _, file := range files {
		base := strings.TrimSuffix(path.Base(file), ".sql")
		parts := strings.SplitN(base, "_", 2)
		if len(parts) != 2 {
			return nil, fmt.Errorf("invalid migration name: %s", file)
		}
		version, err := strconv.Atoi(parts[0])
		if err != nil || version 

这段解析还做了两件容易被忽略的事:拒绝没有描述名的文件,并拒绝重复版本。即使两份文件的名称不同,只要版本相同,执行顺序就存在歧义,应在连接数据库前直接失败。

用版本表判断哪些迁移还没执行

版本表至少保存版本号、可读名称和应用时间。版本号做主键,可以阻止同一个数据库出现两条相同版本记录,但它不能单独解决两个实例同时执行 DDL 的竞态。

CREATE TABLE IF NOT EXISTS schema_migrations (
    version BIGINT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

启动时先查询每个版本是否存在。只有不存在时才打开事务;不要先执行结构变更,随后在另一个连接里补写版本记录,否则中途失败会让数据库结构和版本表失去一致性。

让结构变更和版本登记共享事务边界

迁移文件、版本号、sql.Tx、业务表结构和 schema_migrations 的静态数据关系图
图2:迁移事务与数据库状态关系图,不是运行结果。
package migrate

import (
	"context"
	"database/sql"
	"fmt"
)

func Apply(ctx context.Context, db *sql.DB) error {
	items, err := loadMigrations()
	if err != nil {
		return err
	}

	for _, item := range items {
		var exists bool
		// 版本表是数据库端事实来源,已应用版本直接跳过。
		err := db.QueryRowContext(ctx,
			"SELECT EXISTS (SELECT 1 FROM schema_migrations WHERE version = ?)",
			item.Version,
		).Scan(&exists)
		if err != nil {
			return fmt.Errorf("check migration %d: %w", item.Version, err)
		}
		if exists {
			continue
		}

		body, err := migrationFS.ReadFile(item.Path)
		if err != nil {
			return fmt.Errorf("read migration %d: %w", item.Version, err)
		}
		if err := applyOne(ctx, db, item, string(body)); err != nil {
			return err
		}
	}
	return nil
}

func applyOne(ctx context.Context, db *sql.DB, item Migration, statement string) (err error) {
	tx, err := db.BeginTx(ctx, nil)
	if err != nil {
		return fmt.Errorf("begin migration %d: %w", item.Version, err)
	}
	// Commit 成功前的任何返回都会尝试回滚。
	defer func() { _ = tx.Rollback() }()

	// 迁移 SQL 和版本登记必须使用同一个 tx。
	if _, err = tx.ExecContext(ctx, statement); err != nil {
		return fmt.Errorf("execute migration %d: %w", item.Version, err)
	}
	if _, err = tx.ExecContext(ctx,
		"INSERT INTO schema_migrations(version, name) VALUES (?, ?)",
		item.Version, item.Name,
	); err != nil {
		return fmt.Errorf("record migration %d: %w", item.Version, err)
	}
	if err = tx.Commit(); err != nil {
		return fmt.Errorf("commit migration %d: %w", item.Version, err)
	}
	return nil
}

示例中的占位符 ? 适用于部分驱动;PostgreSQL 一般使用 $1、$2。实际项目应把版本表 SQL 和占位符收敛到数据库方言层,而不是到处拼接。

三个生产边界必须提前决定

边界风险建议
一个文件多条语句驱动可能禁止一次 Exec 多语句,简单分号切割又会破坏函数体或字符串每个文件保持驱动支持的执行单元,或使用成熟 SQL 解析器/迁移库
DDL 事务部分数据库会对某些 DDL 隐式提交,Rollback 无法恢复按目标数据库确认语义,必要时拆分迁移并设计补偿
并发部署两个实例可能都看到“未执行”,随后同时改表只允许一个部署任务迁移,或使用数据库专用 advisory lock

版本主键只能在插入记录时发现冲突,不能保证之前的 DDL 没有被两个实例同时执行。因此并发控制必须放在迁移循环之外,并覆盖“检查、执行、登记”的完整区间。

迁移文件发布后只追加,不修改

内嵌资源会跟随二进制版本变化。如果修改已经在生产库执行过的 0002_add_email.sql,旧数据库的版本表仍显示 2 已应用,新数据库却会执行修改后的内容,最终形成同一版本号对应两种结构。正确做法是保留旧文件,新增 0003_...。对审计要求高的系统还可以在版本表保存文件哈希,并在启动时核对已应用版本的内容是否被改动。

上线前至少检查:版本号唯一且连续策略明确、文件名能按字典序稳定排序、迁移账号权限足够、失败后能安全重试、备份或回滚方案可用。若需求包括向下迁移、脏状态恢复、多数据库方言和复杂锁管理,直接采用成熟迁移库通常比继续扩展这个小执行器更可靠。

常见问题

为什么已经用四位数字命名,还要 sort.Strings?

固定宽度负责让字典序符合版本序,显式排序负责把执行器的假设写进代码。两者结合后,目录读取方式变化也不容易影响结果。

可以在程序每次启动时自动执行吗?

小型单实例服务可以,但多实例滚动发布更适合由唯一部署任务执行,应用实例只检查版本是否满足要求,避免启动风暴触发并发迁移。

为什么不直接把 SQL 按分号切开?

分号可能出现在字符串、存储过程或触发器定义中,简单切割会产生错误语句。需要多语句解析时应使用了解目标数据库语法的工具。

embed.FS 能在运行时替换迁移文件吗?

不能。内嵌文件在编译时确定,替换内容需要重新构建二进制。这正好让迁移集合与发布制品绑定,但不适合需要动态下发脚本的系统。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
合成数据能否替代真实样本:覆盖率与偏差检查方法合成数据能否替代真实样本:覆盖率与偏差检查方法
上一篇
合成数据能否替代真实样本:覆盖率与偏差检查方法
嵌入资源更新后程序仍读到旧内容,构建缓存应如何排查
下一篇
嵌入资源更新后程序仍读到旧内容,构建缓存应如何排查
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    381次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    452次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    463次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    403次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    232次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码