在二进制中嵌入迁移文件并按版本顺序执行
最稳妥的做法是:用 //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 目录。

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

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 能在运行时替换迁移文件吗?
不能。内嵌文件在编译时确定,替换内容需要重新构建二进制。这正好让迁移集合与发布制品绑定,但不适合需要动态下发脚本的系统。
合成数据能否替代真实样本:覆盖率与偏差检查方法
- 上一篇
- 合成数据能否替代真实样本:覆盖率与偏差检查方法
- 下一篇
- 嵌入资源更新后程序仍读到旧内容,构建缓存应如何排查
-
- Golang · Go教程 | 2小时前 |
- 为前端资源建立开发期本地读取与发布期嵌入切换
- 275浏览 收藏
-
- Golang · Go教程 | 2小时前 | 单元测试 · Go教程 · 工程实践 · html/template embed.FS fs.FS Go嵌入资源 fstest.MapFS
- 用 embed.FS 打包静态模板并保持目录结构可测试
- 230浏览 收藏
-
- Golang · Go教程 | 2小时前 | docker · CGO · Go教程 · CGO_ENABLED Docker Buildx cgo交叉编译 Go交叉编译镜像 多架构镜像 GNU交叉编译器
- 为含 cgo 的项目设计可重复的交叉编译镜像
- 480浏览 收藏
-
- Golang · Go教程 | 3小时前 | CGO · 资源管理 · Go教程 · runtime.KeepAlive runtime/cgo.Handle Go cgo C库句柄 LockOSThread
- 封装 C 库句柄并明确创建、释放与线程约束
- 400浏览 收藏
-
- Golang · Go教程 | 4小时前 | 模块 · go · CI · Go 持续集成 govulncheck 依赖安全
- 在持续集成中生成依赖清单并跟踪安全更新
- 244浏览 收藏
-
- Golang · Go教程 | 4小时前 | Go教程 · 调用栈 govulncheck Go依赖安全 漏洞可达性 Go漏洞数据库
- 用 govulncheck 区分被依赖漏洞与实际可达调用
- 283浏览 收藏
-
- Golang · Go教程 | 5小时前 | 并发 · go · goroutine阻塞 go tool trace Go执行跟踪 调度延迟 runtime trace
- 借助执行跟踪分析调度延迟和阻塞来源
- 228浏览 收藏
-
- Golang · Go教程 | 5小时前 | go · 性能优化 · 垃圾回收 · pprof · Go性能分析 pprof alloc_space inuse_space Go堆快照
- 结合堆快照区分瞬时分配与长期持有
- 382浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 381次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 452次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 463次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 403次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 232次使用
-
- Java 性能优化上线清单:从定位、改造到灰度发布
- 2026-06-11 860浏览
-
- Spring Boot 压测验证:Gatling、JMeter 与性能回归门禁
- 2026-06-11 843浏览
-
- Java NMT 非堆内存排查:Direct Buffer、线程栈与 Metaspace 分析
- 2026-06-11 826浏览
-
- Spring Boot 容器内存优化:JVM 堆、非堆与 MaxRAMPercentage
- 2026-06-11 809浏览
-
- Tomcat 连接与线程参数调优:maxThreads、acceptCount 与 KeepAlive
- 2026-06-11 792浏览

