golang-migrate, goose, dbmate and Atlas are all written in Go, all ship as a single binary, and all run plain SQL migrations against the usual databases, which is why they get compared and why the comparison is confusing. They differ in file layout, in how they record and protect what has been applied, in how well they embed into a Go program, and in ambition: three are migration runners, the fourth is a schema management platform that also runs migrations. Here is what actually separates them.
File layouts at a glance#
| Tool | File names | Up and down | Extra files |
|---|---|---|---|
| golang-migrate | 000001_create_owners.up.sql | Separate .up.sql and .down.sql files | None |
| goose | 00001_create_owners.sql | -- +goose Up and -- +goose Down sections in one file | Optional Go migrations |
| dbmate | 20240101000001_create_owners.sql | -- migrate:up and -- migrate:down sections in one file | Optional schema.sql dump |
| Atlas | 20240101000001_create_owners.sql | Up only; down is computed or written separately | atlas.sum, required |
The same table in goose's format shows the section markers:
-- +goose Up
CREATE TABLE owners
(
id INT NOT NULL,
first_name VARCHAR(50) NOT NULL,
last_name VARCHAR(50) NOT NULL,
CONSTRAINT pk_owners PRIMARY KEY (id)
);
-- +goose Down
DROP TABLE owners;
dbmate reads almost identically with -- migrate:up and -- migrate:down. golang-migrate wants two files per version and treats a missing down file as "cannot go down". Atlas keeps one file per version, forward only, and adds a sum file that lists every migration with a hash:
h1:TpOtWuqhxQtvcIUEVxDE0tvMrRMZ5dB6ucp2WP79yBI=
20240101000001_create_owners_and_pets.sql h1:NLDvb6gR+TYWXAAkeR19cVSIgvT0DLsMo3QieF6e7c0=
20240101000002_create_vets.sql h1:XCP0DWSgFxuc1Cd/fl7DOTpce2kAzUT9n/Q+wyfqkmg=
20240101000003_create_visits.sql h1:xJZY+MigCeKok3XcK2gU/iq7XTGY9HiN9qceVRBt+wU=
Version numbers are just sortable strings to all four. Sequential integers read better; timestamps avoid collisions between branches. goose and dbmate generate timestamps by default (goose create, dbmate new), golang-migrate's migrate create -seq gives sequential numbers, and Atlas generates timestamps.
How applied state is recorded#
Each tool keeps its own bookkeeping table, and the shape of that table tells you how the tool thinks:
- golang-migrate keeps one row in
schema_migrations: the current version and adirtyflag. It does not remember which versions ran, only where it is. A failed migration setsdirty, and nothing runs again until a human fixes the database and runsmigrate force N. - goose keeps a row per applied migration in
goose_db_version, withversion_idandis_applied, so it can report history and detect migrations that were applied out of order. - dbmate keeps a row per applied version in
schema_migrations, the same table name as golang-migrate but a different shape, which matters if you ever switch between them. - Atlas keeps a row per applied file in
atlas_schema_revisions, including the hash of the file at the time it ran and how far a partially applied file got, so it can resume a failed migration at the statement that failed.
Integrity and drift#
Only Atlas checks that the migration files themselves have not changed. The atlas.sum file is regenerated with atlas migrate hash and verified before every run; edit a migration that has been applied and Atlas refuses to continue until the hash is updated deliberately, the same protection Flyway and Liquibase give through stored checksums. atlas migrate lint goes further and analyses new migrations for destructive changes and statements that take long locks. That pairs naturally with the concerns in schema drift: detection and prevention.
golang-migrate, goose and dbmate trust the directory. Editing an applied file is not detected, and out-of-order versions are handled differently: goose has an -allow-missing flag to apply an older version that arrived late, golang-migrate applies strictly upward from the current version and never looks back, dbmate applies any pending version by default. dbmate compensates with dbmate dump, which writes the full schema to db/schema.sql after every migration; committing that file makes drift visible in code review, even though the tool does not enforce anything.
Transactions and statement handling#
goose wraps each migration in a transaction unless the file says -- +goose NO TRANSACTION, and needs -- +goose StatementBegin and StatementEnd around bodies that contain semicolons, such as PL/pgSQL functions. dbmate also wraps each migration by default and accepts -- migrate:up transaction:false for statements that cannot run inside one. Atlas runs each file in a transaction by default, with a per-file -- atlas:txmode none directive to opt out. golang-migrate does not wrap a migration in a transaction on its own; add BEGIN and COMMIT to the file when the whole script must succeed or fail together. On MySQL and MariaDB none of this buys much, because their DDL commits implicitly, a point transactional DDL explained covers in detail.
Embedding in a Go binary#
If the service should migrate its own database on start-up, the tool has to be a library too. golang-migrate is, through the iofs source and Go's embed:
//go:embed migrations/*.sql
var migrations embed.FS
func migrateUp(databaseURL string) error {
source, err := iofs.New(migrations, "migrations")
if err != nil {
return err
}
m, err := migrate.NewWithSourceInstance("iofs", source, databaseURL)
if err != nil {
return err
}
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
return err
}
return nil
}
goose is equally at home as a library (goose.SetBaseFS plus goose.Up) and adds Go migrations, functions with the signature func(ctx context.Context, tx *sql.Tx) error registered from an init, for changes that need application code such as re-encoding a column. dbmate exposes a small Go API as well. Atlas is a CLI first; it has a Go SDK, but its centre of gravity is the command line, the CI integrations and the paid cloud service.
Database support#
All four handle PostgreSQL, MySQL, MariaDB and SQLite. golang-migrate and goose reach further, to SQL Server, ClickHouse, Cassandra and others through pluggable drivers. dbmate covers PostgreSQL, MySQL, SQLite and ClickHouse among others and has no SQL Server driver. Atlas supports SQL Server only in its commercial edition. None of the four offers Oracle out of the box. If your fleet includes SQL Server or Oracle, that alone narrows the list.
Where each fits#
- golang-migrate: you want the most widely used option, a library and a CLI in one, and you accept the single-version model with its dirty flag. Good default for services that migrate themselves.
- goose: you want per-migration history, transactions by default, Go migrations for the awkward cases, and a friendlier CLI. Good default for teams that run migrations from the binary or from CI.
- dbmate: you want a language-agnostic CLI with almost no configuration, a committed
schema.sql, and a team that includes non-Go services. Good for polyglot repositories. - Atlas: you want integrity hashes, linting, a declarative mode where the desired schema is the source of truth, and a CI story; you are comfortable with a tool that has a company behind it. Good for platform teams standardising many services.
Whichever you choose, the migration directory becomes the definition of your schema, exactly as it does for the JVM tools compared in Flyway vs Liquibase.
Where SchemaDrawer fits#
All four tools are supported by SchemaDrawer, which runs the migration directory with the tool itself against a throw-away database of the engine and version you pick and returns an ER diagram of the resulting tables, columns, primary keys and foreign keys. Upload the directory as it is: golang-migrate's down files are ignored, goose and dbmate down sections are not applied, and Atlas needs its atlas.sum to be present and current, exactly as it does on your machine. The per-tool pages for golang-migrate, goose, dbmate and Atlas list the details.
Key takeaways#
- Layouts differ in small ways that matter: two files, one file with markers, or one file plus a sum file.
- golang-migrate records a position; goose, dbmate and Atlas record history.
- Only Atlas verifies that applied files have not been edited.
- goose, dbmate and Atlas wrap migrations in transactions by default; golang-migrate leaves it to you.
- SQL Server and Oracle support decides the list before features do.