Go Goose 怎样把 SQL 迁移文件嵌入 Go 可执行文件
来源:17golang原创
时间:2026-10-29 11:20:22 456浏览 收藏
Goose 可以读取 Go 的 embed.FS:用 //go:embed migrations/*.sql 把迁移文件编入程序,再通过 goose.SetBaseFS 指定这个文件系统,调用 goose.UpContext(ctx, db, "migrations")。目录参数依然要写,因为内置文件系统保留了 migrations 这一层。
官方地址:https://github.com/pressly/goose
如果发布目标是“程序和 SQL 必须来自同一次构建”,我会优先选嵌入方案。这个判断来自交付约束,而不是某次线上事故:下面以一个独立的 PostgreSQL 迁移命令为例,说明文件、连接和执行职责怎样拆开。示例用于讲解,未附实际运行输出。
先按交付约束选择嵌入方案
我最看重的是减少交付项。SQL 和程序一起构建后,镜像无需再维护一份 migrations 目录的复制规则。但如果团队要求不重新构建就替换迁移脚本,外部目录更合适,嵌入反而增加发布成本。
| 考虑点 | 嵌入 SQL | 外部目录 |
|---|---|---|
| 交付对象 | 程序包含构建时的脚本副本 | 程序和脚本目录分别交付 |
| 更新脚本 | 重新编译、重新发布 | 更新目录及其版本管理 |
| 运行时路径 | 内置文件系统路径 | 操作系统目录路径 |
| 适合的约束 | 迁移内容随应用版本冻结 | 脚本独立交付和管理 |
Goose 同时提供 CLI 和库。这里使用库构建自己的命令;单独安装通用 goose 命令,并不会自动把你的项目 SQL 打包进去。项目说明列出了嵌入迁移支持。
让迁移路径属于源码包
建立一个独立的迁移目录:cmd/dbmigrate/main.go 保存下面的 Go 程序,cmd/dbmigrate/migrations/00001_create_task_labels.sql 保存 SQL。两者的相对位置才是关键。embed 模式相对于声明它的源码包目录解释,不是相对于你敲 go build 命令的位置。
-- +goose Up
-- 示例业务表:记录任务使用的标签,不包含连接凭据。
CREATE TABLE task_labels (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
label TEXT NOT NULL UNIQUE
);
-- +goose Down
-- 回退会删除标签数据,仅在明确接受数据损失时使用。
DROP TABLE task_labels;00001 是迁移版本前缀,-- +goose Up 标识升级内容,Down 部分说明回退操作。本例只创建一张普通表,使用 Goose 默认事务安排;不要把这种安排扩大成所有数据库 DDL 都能事务回滚的结论。
声明 migrations/*.sql 会保留匹配文件的目录层级。embed 路径使用正斜线,不能通过 ../ 跨到父目录。把 SQL 放到声明所在包的子目录,通常比依赖构建机的绝对路径清楚。更多模式限制见 Go embed 文档。

把内置文件系统接入迁移程序
下面的完整 main.go 使用 PostgreSQL 的 database/sql 驱动。项目需要声明 Goose v3 和 pgx v5 依赖,并把选择的具体版本固定在 go.mod、go.sum 中。embed 的语法支持和所选依赖版本的最低 Go 工具链要求是两回事,构建环境应满足后者。
package main
import (
"context"
"database/sql"
"embed"
"fmt"
"log"
"os"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/pressly/goose/v3"
)
// 只嵌入随本次构建发布的 SQL,路径相对于当前源码包。
//go:embed migrations/*.sql
var migrationFiles embed.FS
func migrate() error {
dsn := os.Getenv("MIGRATION_DATABASE_URL")
if dsn == "" {
return fmt.Errorf("缺少 MIGRATION_DATABASE_URL")
}
db, err := sql.Open("pgx", dsn)
if err != nil {
return fmt.Errorf("创建数据库连接池失败: %w", err)
}
// 在返回上层之前释放连接池;不要在此函数里直接退出进程。
defer func() {
if err := db.Close(); err != nil {
log.Printf("关闭迁移连接池失败: %v", err)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()
if err := db.PingContext(ctx); err != nil {
return fmt.Errorf("连接目标数据库失败: %w", err)
}
// 本命令只有一个迁移入口,初始化后不再并发切换包级配置。
goose.SetBaseFS(migrationFiles)
if err := goose.SetDialect("postgres"); err != nil {
return fmt.Errorf("设置数据库方言失败: %w", err)
}
// migrations 是内置文件系统中的目录,不是部署机器上的绝对路径。
if err := goose.UpContext(ctx, db, "migrations"); err != nil {
return fmt.Errorf("执行内置迁移失败: %w", err)
}
return nil
}
func main() {
if err := migrate(); err != nil {
// migrate 中的 defer 已执行,错误退出可交给发布系统处理。
log.Print(err)
os.Exit(1)
}
log.Print("迁移命令完成")
}我会把三个参数分别记住:migrationFiles 决定从哪里发现脚本;"postgres" 决定 Goose 使用的数据库方言;db 才代表真实数据库连接。pgx 驱动的注册名与 Goose 方言名不同,不能因为连接池用 pgx 就把方言字符串也改为 pgx。
上述接口及 UpContext 的参数可查 Goose API 文档。这是一个专用命令的包级 API 示例,多套互不相同的迁移配置不宜在并发任务中反复调用全局设置。
把数据库资源留在发布配置中
程序里包含的是 SQL 字节,不是数据库。MIGRATION_DATABASE_URL、网络访问、目标数据库、迁移账号权限仍需由部署环境提供。连接串不要写进 SQL 或作为 embed 文件发布;能读取程序的人不应因此获得部署凭据。

图中的 migrationFiles 与 SetBaseFS 相连,SetDialect 与 UpContext 相连;环境连接串配置 sql.DB,后者也是 UpContext 的依赖。文件嵌入不会消除这些运行条件。
在独立命令里设置两分钟超时,是本文的示例选择,实际应按迁移规模安排。上下文取消是停止工作的请求,不能当成“数据库必定已恢复原状”的证明。迁移失败后先确认目标库状态,再决定修复或重新执行。
发布时守住版本和执行责任
推荐将这个命令作为发布阶段的单独任务,由发布系统明确指定执行者。嵌入只统一了制品内容,并没有自动实现多副本协调;不要把每个应用副本同时启动迁移当作文件嵌入功能的一部分。
提交新 SQL 后重新构建 ./cmd/dbmigrate。构建过程需要包含 migrations 子目录,发布制品则可以只包含构建出的命令及其正常运行依赖。是否能使用完全空白的容器基础镜像,还受驱动、证书和系统依赖影响,不能仅凭 embed 下结论。
已发布的旧迁移应保持稳定,用新的版本文件追加变更。只修改已执行版本的 SQL 内容,不会自动表达一项新的数据库升级。对于不可逆的数据变更,回退计划还应覆盖备份或补偿,本文的 DROP TABLE 示例不能替代生产恢复方案。
为什么 SQL 已经嵌入,还找不到目录?
先比较声明中的 migrations/*.sql 与 UpContext 的目录参数。普通 embed.FS 保留 migrations 层级,所以这里传 "migrations";如果你另外用 fs.Sub 改变了根目录,则必须按改变后的根重新选择目录,不能机械照抄。
运行后修改旁边的 SQL 文件会生效吗?
不会改变这个程序已经嵌入的副本。修改源码中的 SQL 后需要重新构建并部署对应制品。我的取舍是:希望脚本与程序绑定,就接受这项构建成本;希望独立热更新脚本,就选择外部目录管理。
可以往 embed.FS 里 create 新迁移吗?
嵌入资源提供只读访问。Goose 的 create、fix 等修改操作仍针对操作系统文件,不会重写正在运行的可执行文件。开发阶段在源码目录创建迁移,发布阶段读取编译进去的迁移,职责分开后更容易维护。这个限制见 官方嵌入说明。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
317 收藏
-
113 收藏
-
419 收藏
-
112 收藏
-
394 收藏
-
479 收藏
-
204 收藏
-
357 收藏
-
102 收藏
-
422 收藏
-
331 收藏
-
241 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习