Migrations with golang-migrate & goose
Schema changes belong in versioned migration files, not ad hoc ALTER in application startup.
Search across all documentation pages
Schema changes belong in versioned migration files, not ad hoc ALTER in application startup.
golang-migrate and goose are popular Go-friendly tools that apply ordered SQL (or Go) migrations and support rollbacks in CI and deploy pipelines.
Migrations are timestamped or sequenced files with up and down sections.
Apply migrations before new code serves traffic, or use expand-contract patterns for zero-downtime deploys.
Run migration jobs in CI against ephemeral databases to catch SQL errors early.
Never run migrations inside per-request HTTP handlers.
Quick-reference recipe card - copy-paste ready.
# golang-migrate CLI
migrate -path ./migrations -database "${DATABASE_URL}" up
migrate -path ./migrations -database "${DATABASE_URL}" down 1
# goose CLI
goose -dir ./migrations postgres "$DATABASE_URL" up
goose -dir ./migrations postgres "$DATABASE_URL" down// embed migrations (see sibling article)
//go:embed migrations/*.sql
var migrationFS embed.FSWhen to reach for this:
down migrations restore prior shape in staging.package main
import (
"database/sql"
"fmt"
"log"
"github.com/pressly/goose/v3"
_ "github.com/jackc/pgx/v5/stdlib"
)
func main() {
db, err := sql.Open("pgx", "postgres://localhost:5432/app?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
goose.SetDialect("postgres")
if err := goose.Up(db, "migrations"); err != nil {
log.Fatal(err)
}
fmt.Println("migrations applied")
}Example migrations/00001_create_users.sql:
-- +goose Up
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email TEXT NOT NULL UNIQUE
);
-- +goose Down
DROP TABLE users;What this demonstrates:
goose.Up applies pending files in lexical order.SetDialect selects SQL quirks for Postgres, MySQL, or SQLite.schema_migrations or goose version table).up runs pending files; down reverses the latest or N steps.000001_init.up.sql and 000001_init.down.sql.| Step | Owner | Action |
|---|---|---|
| PR | Developer | Add migration + app code in same PR |
| CI | Pipeline | Spin ephemeral DB, up, run tests, optional down |
| Staging | Release | up before rolling pods |
| Production | Release | up with backup snapshot; monitor locks |
Breaking changes (NOT NULL without default, rename in place) need multi-phase releases.
| Tool | Strength | Trade-off |
|---|---|---|
| golang-migrate | Widely used, CLI + library | Go migrations need separate package layout |
| goose | Go migrations in same module | Team must agree on annotation format |
| GORM AutoMigrate | Fast prototypes | Weak review story for production schema |
down in review for reversible changes; document irreversible ops.up before tests.down as staging-only; prod rollbacks restore backups.| Alternative | Use When | Don't Use When |
|---|---|---|
| Flyway/Liquibase (JVM) | Polyglot org standard | Pure Go shop wants embedded migrations |
| GORM AutoMigrate | Hackathon schema | Audited production schema |
| Manual DBA runbooks | Rare touch mainframes | Fast-moving product teams |
| sqlc without migrations | Queries only | You still need schema versioning somewhere |
| Atlas / skeema | Declarative desired state | Team prefers imperative up/down files |
Library embed plus Up on boot is acceptable for small services.
Larger teams prefer a Kubernetes Job or release hook so app pods start only after schema is ready.
Use zero-padded sequence or UTC timestamps; never reuse numbers.
One logical change per file eases review.
Pick one tool per repository to avoid competing version tables.
Migrating tools is a one-time project with a freeze window.
Separate seed scripts or goose Go migrations for dev fixtures.
Production seeds belong in idempotent jobs, not random SQL in up.
Often forward-fix with a new migration instead of down.
down is most valuable in CI and developer laptops.
CLI and jobs may use context.Background() with ops-controlled cancel.
Do not tie schema changes to HTTP request ctx.
CI: up, integration tests, down, up again to detect non-idempotent scripts.
Snapshot row counts for data migrations.
DDL-capable role used only in deploy job; app runtime uses lesser privileges.
Separate users reduce blast radius.
See embed for SQL Migrations & Seed Data for shipping files inside the binary.
Useful for complex backfills with logging and batching.
Keep simple DDL in SQL for DBA reviewability.
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).
Reviewed by Chris St. John·Last updated Jul 19, 2026