embed for SQL Migrations & Seed Data
Go 1.16+ embed lets you ship SQL migration files and seed scripts inside the compiled binary.
Search across all documentation pages
Go 1.16+ embed lets you ship SQL migration files and seed scripts inside the compiled binary.
Deploy artifacts stay self-contained: the same container image that runs the API can apply schema versions without mounting a host directory.
//go:embed attaches files or directories to variables of type embed.FS.
Migration libraries accept io/fs or http.FileSystem adapters over the embedded FS.
Seed data for dev and test can ship in a separate embedded tree from production migrations.
Quick-reference recipe card - copy-paste ready.
package migrations
import (
"embed"
"io/fs"
)
//go:embed sql/*.sql
var files embed.FS
func FS() fs.FS {
sub, _ := fs.Sub(files, "sql")
return sub
}import (
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
source, _ := iofs.New(migrations.FS(), ".")
m, _ := migrate.NewWithSourceInstance("iofs", source, dbURL)
_ = m.Up()When to reach for this:
package main
import (
"database/sql"
"embed"
"fmt"
"io/fs"
"github.com/pressly/goose/v3"
_ "github.com/mattn/go-sqlite3"
)
//go:embed migrations/*.sql
var migrationFiles embed.FS
func main() {
db, _ := sql.Open("sqlite3", ":memory:")
defer db.Close()
goose.SetBaseFS(migrationFiles)
goose.SetDialect("sqlite3")
if err := goose.Up(db, "migrations"); err != nil {
panic(err)
}
var n int
_ = db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table'`).Scan(&n)
fmt.Println("tables:", n)
}migrations/00001_init.sql:
-- +goose Up
CREATE TABLE widgets (id INTEGER PRIMARY KEY, name TEXT);
-- +goose Down
DROP TABLE widgets;What this demonstrates:
go:embed globs SQL next to the Go source file.goose.SetBaseFS reads migrations from memory instead of disk.iofs source driver.embed.FS is read-only and safe to share across goroutines.fs.Sub narrows the root when SQL lives in a subdirectory.| Path | Purpose |
|---|---|
migrations/*.sql | Versioned up/down schema |
seed/dev/*.sql | Local fixtures (optional separate embed) |
testdata/schema.sql | Integration test bootstrap |
internal/migrations | Package exporting FS() for main |
d, err := iofs.New(migrations.FS(), ".")
m, err := migrate.NewWithInstance("iofs", d, "postgres", dbURL)"iofs" pairs with database driver name in NewWithInstance.//go:embed seed/* imported only from main_dev.go.INSERT ... ON CONFLICT DO NOTHING or goose Go migration with loops.go:embed.fs.FS.go run from module root.| Alternative | Use When | Don't Use When |
|---|---|---|
| ConfigMap volume (K8s) | Ops wants hot-patch SQL without rebuild | You need immutable release artifacts |
| Git checkout in container | Simple Dockerfile COPY migrations | Distroless images without shell |
| Remote migration service | Central schema registry | Small teams want embedded simplicity |
go:generate SQL bundling | Custom codegen pipeline | Std embed is enough |
| Flyway on JVM sidecar | Enterprise standard | Pure Go deploy unit |
Yes - tests import the same package and call goose.Up against ephemeral databases.
Keep migration files in the package under test.
Use separate variables and //go:embed directives per directory.
all: prefix embeds hidden files if needed.
Build tags on files (//go:build dev) or separate commands/packages.
Prod main imports only production migration package.
//go:embed migrations embeds the tree; use fs.Sub to set goose directory root.
Verify board targets at build; WASM and embedded builds may have size limits.
Test migration size on constrained targets.
go test that runs Up and asserts tables exist.
Fail build if pattern is empty.
Go migration files compile into the binary naturally; SQL uses embed.FS.
Pick one style per repo for review clarity.
CLI on laptop can still use -path ./migrations while production binary uses iofs - keep files identical in git.
Same down files as disk workflow; embed only changes transport.
Rollback discipline unchanged.
Practical limit is binary size and memory, not Go syntax.
Split large data migrations into batched Go migrations.
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 18, 2026