goose 迁移笔记:SQL 文件写法、版本号策略与内嵌进二进制
pressly/goose 是 Go 写的数据库迁移工具,既是 CLI 也是库,MIT 协议,GitHub 上约 11.6k star。项目地址:,文档站:。它支持 Postgres、MySQL、MariaDB、Spanner、SQLite、YDB、ClickHouse、MSSQL、Vertica 等数据库,迁移可以写成 SQL 文件,也可以写成 Go 函数;此外还支持内嵌迁移、out-of-order、种子数据,以及 SQL 文件里的环境变量替换。
安装
go install github.com/pressly/goose/v3/cmd/goose@latest
brew install goose
需要精简二进制时,用构建标签排除不需要的驱动:
go build -tags='no_postgres no_mysql no_sqlite3 no_ydb' -o goose ./cmd/goose
可用的标签有:no_azuresql no_clickhouse no_libsql no_mssql no_mysql no_postgres no_sqlite3 no_vertica no_ydb。注意,no_mssql 也会同时排除 azuresql。
命令
调用形式是 goose DRIVER DBSTRING [OPTIONS] COMMAND,也可以先把 GOOSE_DRIVER / GOOSE_DBSTRING / GOOSE_MIGRATION_DIR 设好。驱动名包括 postgres mysql sqlite3 spanner mssql azuresql redshift tidb clickhouse ydb starrocks turso。
| 命令 | 作用 |
|---|---|
up | 应用全部未应用的迁移,每个文件打印 OK |
up-to VERSION | 升到指定版本 |
up-by-one | 只升一个版本 |
down | 回滚最近一个迁移 |
down-to VERSION | 回滚到指定版本,down-to 0 回滚全部(慎用) |
redo | 回滚最近一个再重新应用 |
reset | 回滚全部 |
status | 打印 Applied At / Migration,未应用的显示 Pending |
version | 打印当前版本 |
create NAME [sql|go] | 生成迁移文件 |
fix | 把时间戳版本改成顺序号 |
validate | 校验迁移 |
env | 打印环境变量配置 |
create 的两种编号:
goose create add_some_column sql # 20170506082420_add_some_column.sql
goose -s create add_some_column sql # 00001_add_some_column.sql
SQL 迁移文件的结构
每个文件必须恰好有一个 -- +goose Up 注解,-- +goose Down 可选;两者都有时,Up 必须写在前面。Up 之后的语句用于正向迁移,Down 之后的语句用于回滚。
默认整个文件运行在一个事务里。CREATE DATABASE 这类不能在事务中执行的语句,要在文件顶部加 -- +goose NO TRANSACTION,这个文件里的 Up 和 Down 都会跳过事务。
语句以分号切分,查询语句也要以分号结尾。PL/pgSQL 这类函数体内部带分号的语句,用 -- +goose StatementBegin 和 -- +goose StatementEnd 把相关片段包起来。
环境变量替换默认关闭,需要先用 -- +goose ENVSUB ON 打开,一直到 -- +goose ENVSUB OFF 关闭;同一个文件里可以多次开关。支持的写法有 ${VAR} 或 $VAR、${VAR:-default}、${VAR-default}、${VAR?err_msg};${VAR:?err_msg} 不支持。在 PL/pgSQL 里,建议只把需要替换的片段包起来,避免 $$ 被转义处理。
MySQL 的两个连接参数
用 mysql 驱动时必须带上 parseTime=true;一个 SQL 文件里写了多条分号分隔的语句时,还必须带 multiStatements=true,否则多条语句跑不了。
goose mysql "user:password@/dbname?parseTime=true&multiStatements=true" status
版本号:开发用时间戳,上线前 fix
create 默认用当前时间戳作为版本号。默认策略不允许 out-of-order:如果补跑一个版本号比当前版本小的缺失迁移,就会直接报错。需要补跑时,用 -allow-missing,或者在库方式里传 goose.WithAllowMissing()(作用于 Up、UpTo、UpByOne)。
推荐的混合做法是:开发阶段用时间戳版本,上线前在 CI pipeline 里跑 goose fix,把待发布的迁移转成顺序号,同时保留按时间戳排序的相对顺序。create -s 则直接生成 00001 这类顺序号。
内嵌到 Go 二进制
用 Go 1.16 的 embed 把 SQL 迁移打进程序:
//go:embed migrations/*.sql
var embedMigrations embed.FS
goose.SetBaseFS(embedMigrations)
goose.SetDialect("postgres")
goose.Up(db, "migrations")
目录参数要带上 embed 时的目录结构,这里传的是 "migrations"。内嵌只用于应用已有迁移,fix / create 仍然操作文件系统。
Go 函数式迁移需要自己编译 goose 二进制:import github.com/pressly/goose,在迁移文件的 init 里 goose.AddMigration(Up, Down) 注册,再用 import _ ".../migrations" 引入,最后调用 goose.Up(db, dir)。迁移文件名必须以数字加下划线开头,且不能以 _test.go 结尾。
func init() {
goose.AddMigration(Up, Down)
}
func Up(tx *sql.Tx) error {
_, err := tx.Exec("UPDATE users SET username='admin' WHERE username='root';")
return err
}
版本表名与非 public schema
-table 指定版本表名,默认是 goose_db_version。迁移不在 public schema 时,要写成 -table='schemaname.goose_db_version'。默认迁移运行在 public schema 上。
环境变量与 .env
支持 GOOSE_DRIVER、GOOSE_DBSTRING、GOOSE_MIGRATION_DIR、GOOSE_TABLE 四个变量:
GOOSE_DRIVER=postgres
GOOSE_DBSTRING=postgres://...
GOOSE_MIGRATION_DIR=./migrations
GOOSE_TABLE=custom.goose_migrations
.env 默认加载,-env=none 关闭,-env path 指定具体文件。
其他常用选项还有 -dir(默认 .)、-no-versioning(按文件顺序执行、不记版本)、-timeout、-allow-missing,以及 MySQL 专用的 -certfile / -ssl-cert / -ssl-key。
取舍
goose 只负责按版本执行迁移、记录已应用版本,不负责生成迁移内容:create 出来的是空壳文件,表结构得自己写。回滚同理,down / down-to 0 执行的是你在 Down 段里写的语句,如果 Down 段没有写清楚回滚逻辑,数据不会被还原。
版本号混用是实际项目里最容易出问题的地方:默认 out-of-order 报错,意味着顺序号和时间戳不能随缘切换,要么统一改成顺序号,要么在补跑时明确加 -allow-missing。ClickHouse 只完整支持部分部署模式,配置需要额外注意(未实测)。