---
title: "Gooseの使い方｜Go製マイグレーションツールv3.29のコマンドと本番運用"
url: "https://www.issoh.co.jp/tech/details/3401/"
published: 2024-08-27
updated: 2026-10-28
categories: ["Go"]
publisher: "株式会社一創"
---

# Gooseの使い方｜Go製マイグレーションツールv3.29のコマンドと本番運用

goose（pressly/goose）は、Go言語で書かれたデータベースマイグレーションツールです。SQLファイルとGoの関数の両方でスキーマ変更を書き、`goose up`・`goose down`などのコマンドで適用と巻き戻しを行います。本記事の出力はすべて、2026年10月11日公開のv3.29.0をGo 1.27.2でビルドし、SQLiteとPostgreSQL 18.6で実行して取得したものです。基本コマンドに加えて、チーム開発で起きるバージョン番号の衝突と、複数のアプリが同時にマイグレーションを走らせたときの競合まで扱います。

## まとめ

- **最初に覚えるのは4つ**：`goose create`（ファイル作成）→`goose up`（適用）→`goose status`（状態確認）→`goose down`（1つ戻す）。
- **接続情報は環境変数か.env**：`GOOSE_DRIVER`・`GOOSE_DBSTRING`・`GOOSE_MIGRATION_DIR`を置けば、毎回DSNを打たずに済みます。.envは既定で読み込まれます。
- **v3.28.0からGo 1.26以上が必要**：古いGoに固定されたCIでは`@v3.27.3`（Go 1.25.7以上）に版を固定します。
- **Goで書いたマイグレーションは自前のバイナリで実行**：`go install`したgooseはGoの関数を実行できません。migrationsパッケージを取り込み`goose.RunContext`を呼ぶ小さなコマンドを作ります。
- **複数人開発はタイムスタンプ＋goose fix**：開発中はタイムスタンプ、本番に出す前にCIで連番へ変換します。適用済みのタイムスタンプ番号をfixで連番に変えると、改名されたファイルがPending扱いになります。
- **アプリ起動時に適用するならProvider APIとロック**：CLIと従来のグローバル関数にはロックがありません。2台が同じ未適用マイグレーションを同時に処理すると、競合によって片方が失敗する可能性があります。

以下、インストールから本番運用の順に、実行結果とあわせて説明します。

## Gooseとは：pressly/gooseの位置付けと同名ツールの区別

「goose」で検索すると、名前が同じ別のソフトウェアが3つ見つかります。本記事が扱うのは1行目のpressly/gooseです。

| 名前         | 用途         | 言語   | リポジトリ                          | 最新版（2026年10月） |
| ---------- | ---------- | ---- | ------------------------------ | ------------- |
| goose（本記事） | DBマイグレーション | Go   | pressly/goose                  | v3.29.0       |
| Goose      | 負荷テスト      | Rust | tag1consulting/goose           | 0.18.1        |
| goose      | AIエージェント   | Rust | aaif-goose/goose（旧block/goose） | v1.56.0       |

Rust製の負荷テストツールを探している場合は[Goose（Rust）の使い方｜負荷テストの導入手順・コマンド引数・メトリクスの読み方](/column/details/3099/)を参照してください。

pressly/gooseはMITライセンスのOSSで、2012年のLiam Staskawicz氏の実装を元に、Vojtech Vitek氏とMichael Fridman氏が開発を続けています。v3.29.0のCLIが受け付けるドライバは`postgres`・`mysql`・`sqlite3`・`spanner`・`mssql`・`azuresql`・`redshift`・`tidb`・`clickhouse`・`ydb`・`starrocks`・`turso`の12種類です。`azuresql`（Microsoft Entra ID認証）はv3.28.0で追加されました。v3.29.0の変更は、YDBの接続文字列で`use_env_credentials`を指定すると環境変数の認証情報を使えるようになった点と依存ライブラリの更新だけです（[v3.29.0のリリースノート](https://github.com/pressly/goose/releases/tag/v3.29.0)）。

## インストールと接続設定：go install・Homebrew・環境変数の優先順位

### go install・brewでの導入とGo 1.26の要件

Goが入っていれば`go install`で入ります。macOSならHomebrewでも入り、2026年10月28日時点のformulaは3.29.0です。Go自体の導入は[Go言語とは｜1.27系の導入からHTTP APIを動かすまでの実装入門](/tech/details/17483/)にまとめています。

```
go install github.com/pressly/goose/v3/cmd/goose@latest
# macOS
brew install goose

goose --version
# goose version: v3.29.0
```

v3.28.0以降のgo.modは`go 1.26.0`を宣言しており、[CHANGELOG](https://github.com/pressly/goose/blob/main/CHANGELOG.md)のv3.28.0の項にも「Minimum Go version is now 1.26」とあります。v3.29.0も同じ要件です。v3.27.0の時点では1.25で、v3.27.3のgo.modは`go 1.25.7`です。CIのGoを1.25系に固定している場合は`goose@v3.27.3`のように版を指定してください。使わないドライバを外して小さく作るなら、リポジトリで`go build -tags='no_mysql no_mssql no_clickhouse' -o goose ./cmd/goose`のようにビルドタグを付けます。

### 接続設定：引数・環境変数・.envの優先順位

基本形は`goose DRIVER DBSTRING [OPTIONS] COMMAND`です。毎回DSNを打つのは手間なので、実務では環境変数か.envファイルに寄せます。

```
# .env（カレントディレクトリに置くと既定で読み込まれる）
GOOSE_DRIVER=postgres
GOOSE_DBSTRING=postgres://app:secret@localhost:5432/app?sslmode=disable
GOOSE_MIGRATION_DIR=./migrations
GOOSE_TABLE=goose_db_version
```

優先順位はフラグ、環境変数、既定値の順です。.envとシェルの環境変数が両方あるときは、シェル側が勝ちます。.envに`GOOSE_DBSTRING=./app.db`を書いたまま`GOOSE_DBSTRING=./other.db goose status`を実行すると、other.dbが新しく作られました。.envを読ませたくない場合は`-env=none`、別名のファイルを読ませる場合は`-env=.env.staging`のように指定します。本番のDSNは.envに書かず、CIやコンテナの環境変数から渡す運用にしておくと、このシェル側優先の挙動とも噛み合います。

実際にどの値が使われるかは`goose env`で表示できます。SQLite用の.envを置いたディレクトリで実行した結果です。

```
$ goose env
GOOSE_DRIVER="sqlite3"
GOOSE_DBSTRING="./app.db"
GOOSE_MIGRATION_DIR="./migrations"
GOOSE_TABLE=""
NO_COLOR="false"
```

パスワード入りのDSNもそのまま表示されるので、CIのログに`goose env`の出力を残さないでください。`GOOSE_TABLE`が空のときは既定の`goose_db_version`が使われます。

## マイグレーションファイルの作成：SQLとGoの2形式

### goose createのファイル名：UTCタイムスタンプと-sの連番

`goose create NAME sql`はマイグレーションディレクトリにファイルを作ります。日本時間10月28日05:04に実行すると`20261027200405_create_posts.sql`になりました。ファイル名のタイムスタンプはUTCです。`-s`を付けると`00001_add_title.sql`のような連番になります。

```
$ goose create create_posts sql
2026/10/28 05:04:05 Created new file: migrations/20261027200405_create_posts.sql

$ goose -s create add_title sql
2026/10/28 05:04:05 Created new file: migrations/00001_add_title.sql
```

1つのディレクトリにタイムスタンプと連番を混ぜると、連番（1）がタイムスタンプより必ず前に並びます。どちらで番号を振るかは後述の「チーム開発のバージョン衝突」で決めます。

### SQLite用SQLマイグレーションとアノテーション

以下はSQLite用の例です。実行前に接続設定を`GOOSE_DRIVER=sqlite3`、`GOOSE_DBSTRING=./app.db`へ変更してください。直前のPostgreSQL用設定では、このトリガー構文を実行できません。

SQLファイルは`-- +goose Up`と`-- +goose Down`のコメントで区切ります。Upはちょうど1つ必要で、Downは省略できます（書く場合はUpより後）。v3.27.0以降、`goose create`が出すテンプレートから`StatementBegin`／`StatementEnd`が外れました。セミコロンを複数含むトリガーや関数を書くときだけ、自分で囲みます。

```
-- +goose Up
CREATE TABLE users (
  id INTEGER PRIMARY KEY,
  email TEXT NOT NULL UNIQUE,
  name TEXT NOT NULL DEFAULT ''
);

-- +goose StatementBegin
CREATE TRIGGER users_lower_email AFTER INSERT ON users
BEGIN
  UPDATE users SET email = lower(NEW.email) WHERE id = NEW.id;
END;
-- +goose StatementEnd

-- +goose Down
DROP TRIGGER users_lower_email;
DROP TABLE users;
```

ほかによく使うアノテーションは2つです。

- `-- +goose NO TRANSACTION`：ファイル全体をトランザクションの外で実行します。PostgreSQLの`CREATE INDEX CONCURRENTLY`はトランザクション内で実行できないため、これを付けたファイルに分けます。
- `-- +goose ENVSUB ON`：以降の`${ADMIN_EMAIL}`のような記述を環境変数で置換します。既定は無効で、`ENVSUB OFF`かファイル末尾まで有効です。

書いたファイルは`goose -v validate`で、DBに触れずに検査できます。アノテーションの無いファイルは`failed to parse migration: unexpected state 0`で終了コード1になるので、CIの最初に置く価値があります。

```
$ goose -v validate
Type   Txn   Up   Down   Name
────   ───   ──   ────   ────
sql    ✔     2    2      20260901000000_create_users.sql
sql    ✔     1    1      20260910000000_add_name.sql
sql    ✘     1    1      20260915000000_email_index.sql
```

Up・Down列はファイル内の文の数です。上のトリガー付きのファイルは、CREATE TABLEとStatementBegin〜Endで囲んだトリガーで2文と数えられています。Txn列が✘の行は`NO TRANSACTION`を付けたファイルです。

### Goマイグレーション：SQLiteの既存行の加工

以下はSQLiteの少量データ向けの例です。対象行をすべてメモリに保持し、1つのトランザクション内で更新します。大量データでは分割処理と再実行方法を別途設計してください。SQL中のプレースホルダーは`?`を使います。PostgreSQLへ移植する場合は`$1`・`$2`へ変更してください。

SQLだけで書きにくい変換、たとえばメールアドレスの@より前を新しいnameカラムに埋める処理は、Goの関数で書けます。ファイル名は数字で始め、`_test.go`で終わらせないのが条件です。

```
package migrations

import (
	"context"
	"database/sql"
	"errors"
	"strings"

	"github.com/pressly/goose/v3"
)

func init() {
	goose.AddMigrationContext(upBackfillName, downBackfillName)
}

func upBackfillName(ctx context.Context, tx *sql.Tx) error {
	rows, err := tx.QueryContext(ctx, `SELECT id, email FROM users WHERE name = ''`)
	if err != nil {
		return err
	}
	type row struct {
		id    int64
		email string
	}
	var targets []row
	for rows.Next() {
		var r row
		if err := rows.Scan(&r.id, &r.email); err != nil {
			rows.Close()
			return err
		}
		targets = append(targets, r)
	}
	if err := rows.Err(); err != nil {
		rows.Close()
		return err
	}
	if err := rows.Close(); err != nil {
		return err
	}
	for _, r := range targets {
		name, _, _ := strings.Cut(r.email, "@")
		if _, err := tx.ExecContext(ctx, `UPDATE users SET name = ? WHERE id = ?`, name, r.id); err != nil {
			return err
		}
	}
	return nil
}

func downBackfillName(ctx context.Context, tx *sql.Tx) error {
	// どの行を埋めたかを記録していないため、自動では安全に戻せない
	return errors.New("backfill cannot be reversed safely; use a forward migration")
}
```

Downをエラーにしているのは、Upで埋めた行と、もともと値が入っていた行や後から利用者が書き換えた行を区別できないためです。`UPDATE users SET name = ''`のように全行を空に戻すDownは、データを消すだけで逆操作になりません。`AddMigrationContext`はトランザクション内、`AddMigrationNoTxContext`はトランザクション外で動きます。

行の加工をアプリの構造体で扱いたくなっても、マイグレーション内ではGORMなどのモデルを使わず、生のSQLで書いてください。モデルの変更によって、古いマイグレーションがコンパイルできなくなったり、生成されるSQLが変わったりする可能性があります。GORMの一括挿入の書き方は[GORM CreateInBatchesの一括挿入とトランザクション範囲｜v1.31.2の基本操作](/tech/details/3402/)で扱っています。

### Goマイグレーションを動かす自前のバイナリ：goose.RunContext

`go install`で入れた`goose`コマンドは、Goマイグレーションを実行できません。Goの関数はコンパイルして初めて登録されるためです。この例ではモジュール名を`example.com/gapp`とし、`github.com/pressly/goose/v3`と`modernc.org/sqlite`を依存関係に追加します。既存プロジェクトではimport内のモジュール名を自分のものに置き換えます。そのうえでmigrationsパッケージを取り込んだ自前のコマンドを作り、`goose.RunContext`にサブコマンドを渡します。公式リポジトリの[examples/go-migrations](https://github.com/pressly/goose/blob/main/examples/go-migrations/main.go)も同じ構成です。

```
// cmd/migrate/main.go
package main

import (
	"context"
	"database/sql"
	"flag"
	"log"

	"github.com/pressly/goose/v3"
	_ "modernc.org/sqlite"

	_ "example.com/gapp/migrations" // init()でGoマイグレーションを登録させる
)

func main() {
	dir := flag.String("dir", "migrations", "マイグレーションのディレクトリ")
	dsn := flag.String("dsn", "app.db", "SQLiteのファイル")
	flag.Parse()
	args := flag.Args()
	if len(args) == 0 {
		log.Fatal("usage: migrate [-dir DIR] [-dsn DSN] COMMAND [ARGS...]")
	}

	db, err := sql.Open("sqlite", *dsn)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	if err := goose.SetDialect("sqlite3"); err != nil {
		log.Fatal(err)
	}
	if err := goose.RunContext(context.Background(), args[0], db, *dir, args[1:]...); err != nil {
		log.Fatal(err)
	}
}
```

`go build -o migrate ./cmd/migrate`で作ったバイナリは、up・down・statusなどRunContextが対応するサブコマンドを受け付けます。CLI専用のenv・validateには対応しません。SQLの`00001_create_users.sql`（前掲のトリガー付きSQLのUp末尾に`INSERT INTO users (email) VALUES ('Taro@Example.com');`を追加）と`00002_backfill_name.go`を置いて実行した結果です。

```
$ ./migrate up
OK   00001_create_users.sql (52.61ms)
OK   00002_backfill_name.go (2.6ms)
goose: successfully migrated database to version: 2

$ ./migrate down
ERROR go migration: "00002_backfill_name.go": failed to run go migration: backfill cannot be reversed safely; use a forward migration
$ ./migrate version
goose: version 2
```

INSERTした`Taro@Example.com`の行は`email=taro@example.com name=taro`になりました（emailの小文字化は前掲のトリガーによるもの）。Downがエラーを返すと終了コード1で止まり、バージョンは2のまま残ります。

デプロイ先には`.go`ファイルを置く必要がありません。バイナリと`.sql`だけをコピーした別ディレクトリで`status`を実行しても、`00002_backfill_name.go`はPendingとして一覧に出て、`up`で適用されました。登録済みのGo関数はバイナリの中にあるためです。

## goose up・down・statusの実行結果：コマンド別の動き

v3.29.0の`goose -h`に並ぶコマンドは13個です。

| コマンド                    | 動作            |
| ----------------------- | ------------- |
| `up`                    | 未適用をすべて適用     |
| `up-by-one`             | 次の1つだけ適用      |
| `up-to VERSION`         | 指定バージョンまで適用   |
| `down`                  | 最新を1つ戻す       |
| `down-to VERSION`       | 指定バージョンまで戻す   |
| `redo`                  | 最新を戻して再適用     |
| `reset`                 | すべて戻す         |
| `status`                | 各ファイルの適用状況    |
| `version`               | 現在のバージョン      |
| `create NAME [sql\|go]` | ファイル作成        |
| `env`                   | 解決後の設定を表示     |
| `fix`                   | タイムスタンプを連番へ変換 |
| `validate`              | 実行せずに検査       |

`up`の前後で`status`を見ると、Pendingが適用日時に変わります。

```
$ goose status
    Applied At                  Migration
    =======================================
    Pending                  -- 20260901000000_create_users.sql
    Pending                  -- 20260910000000_add_name.sql

$ goose up
OK   20260901000000_create_users.sql (2.17ms)
OK   20260910000000_add_name.sql (1.76ms)
goose: successfully migrated database to version: 20260910000000

$ goose status
    Applied At                  Migration
    =======================================
    Tue Oct 27 20:04:04 2026 -- 20260901000000_create_users.sql
    Tue Oct 27 20:04:04 2026 -- 20260910000000_add_name.sql
```

このSQLite実行例のApplied AtはUTCです（日本時間では10月28日05:04）。表示時刻はDBに保存されたタイムスタンプとドライバの扱いに依存し、gooseが一律にUTCへ変換するわけではありません。

### ロールバック：down・down-to・redo・resetの戻り幅

戻す幅で4つを使い分けます。`down`は1つ、`down-to 20260901000000`はそのバージョンが最新になるまで、`down-to 0`と`reset`は全部です。`redo`は最新1つのDownとUpを続けて実行するので、Downの書き漏れを見つけるのに向きます。

```
$ goose down
OK   20260910000000_add_name.sql (2.01ms)
$ goose version
goose: version 20260901000000
```

2件とも適用した状態から`down-to 0`を実行すると、新しい順に全件が戻ります。

```
$ goose down-to 0
OK   20260910000000_add_name.sql (1.75ms)
OK   20260901000000_create_users.sql (22.29ms)
goose: no migrations to run. current version: 0
```

Downは「逆向きのSQL」であって、バックアップではありません。`ALTER TABLE users DROP COLUMN name`を含むDownを本番で実行すると、そのカラムの値は戻りません。本番でデータを消す変更を取り消すときは、Downを流すより、値を残したまま前向きの修正マイグレーションを追加する方が安全です。Downは開発環境とステージングで`redo`を回すための道具と割り切るのが実務的です。

### goose\_db\_versionテーブルに残る記録

適用履歴は`goose_db_version`テーブルに入ります。初回実行時にversion 0の行が作られ、以降は適用ごとに1行ずつ増えます。上の`up`→`down`→`up`→`down-to 0`のあと、もう一度`up`した状態です。

```
$ sqlite3 app.db 'select * from goose_db_version'
1|0|1|2026-10-27 20:04:04
5|20260901000000|1|2026-10-27 20:04:05
6|20260910000000|1|2026-10-27 20:04:05
```

idが2〜4の行はありません。gooseはDownを実行すると、そのバージョンの行を削除します。「いつ戻したか」の履歴はこのテーブルに残らないので、監査が必要ならCIのログで残してください。

列は`id`・`version_id`・`is_applied`・`tstamp`の4つです。v3.28.0以降は、新規に作るMySQL／TiDBのテーブルで`tstamp`が`TIMESTAMP`から`DATETIME`に変わりました。`TIMESTAMP`は2038年1月19日までしか持てないためです。既存テーブルは自動では変わらないので、リリースノートにある`ALTER TABLE goose_db_version MODIFY tstamp datetime NULL DEFAULT CURRENT_TIMESTAMP;`を自分で流します。テーブル名は`-table`か`GOOSE_TABLE`で変えられます。

## チーム開発のバージョン衝突：missing migrationsとgoose fixの使いどころ

複数人がタイムスタンプでファイルを作ると、日付の古いファイルが後からマージされることがあります。9月15日のファイルまで適用済みのDBに、別ブランチで作った9月5日のファイルが入ってきた状況を再現すると、`up`はエラーで止まりました。

```
$ goose up
goose run: error: found 1 missing migrations before current version 20260915000000:
	version 20260905000000: migrations/20260905000000_create_posts.sql
```

`-allow-missing`を付ければ適用されますが、実行順がファイル名の順と食い違います。既存DBでは9月15日→9月5日、新規DBでは9月5日→9月15日の順になるため、両方の順序で同じ結果になることを確認する必要があります。本番では`-allow-missing`を常用しないでください。

pressly/gooseの[README](https://github.com/pressly/goose/blob/main/README.md)が推奨するのは「Hybrid Versioning（ハイブリッドバージョニング）」です。開発中は`goose create`の既定どおりタイムスタンプで作り、本番に出す直前に`goose fix`で連番へ変換します。

```
$ goose fix
RENAMED 20260901000000_create_users.sql => 00001_create_users.sql
RENAMED 20260905000000_create_posts.sql => 00002_create_posts.sql
RENAMED 20260910000000_add_name.sql => 00003_add_name.sql
RENAMED 20260915000000_email_index.sql => 00004_email_index.sql
```

落とし穴は、fixを実行するタイミングです。タイムスタンプ版を適用済みのDBに向けてfix後のファイルで`status`を見ると、4件すべてがPendingになり、`up`は`found 4 missing migrations before current version 20260915000000`で失敗しました。DBには`20260915000000`が記録されているのに、ファイル側は1〜4に改名されているためです。続けて`goose reset`を実行すると、終了コード0で終わるのに何も戻らず、`goose version`は`20260915000000`のままでした。

運用ルールは次の2点に絞れます。

- fixはmainブランチへのマージ時にCIで1回だけ実行し、改名後のファイルをコミットする。本番とステージングには連番しか適用しない。
- タイムスタンプ版を適用済みのローカルDBは、fix後のファイルで`reset`しても元に戻りません。不要な開発用DBを再作成して連番版を適用するか、改名前のファイルでresetしてから連番版を適用します。本番相当のデータを入れたDBはテストコンテナで使い捨てにしておくと楽です（[Testcontainersとは？Dockerで作る使い捨てテスト環境と2.0系の移行点](/tech/details/4629/)）。

1人で開発しているなら、ハイブリッドにする必要はありません。単一ブランチで順に作成するなら、最初から`-s`で連番を振る方法で十分です。ただし、1人でも複数ブランチで並行作業すれば番号が重複することがあります。

## 本番運用：embedで単一バイナリ化とProvider APIのロック

### embed.FS・goose.SetBaseFSによるマイグレーションの同梱

アプリケーションの起動時にマイグレーションを適用する場合、SQLファイルを`embed.FS`でバイナリに埋め込めば、コンテナにmigrationsディレクトリを置かずに済みます。

```
// migrations/embed.go
package migrations

import "embed"

//go:embed *.sql
var FS embed.FS
```

```
package main

import (
	"context"
	"database/sql"
	"log"

	"github.com/pressly/goose/v3"
	_ "modernc.org/sqlite"

	"example.com/gapp/migrations"
)

func main() {
	db, err := sql.Open("sqlite", "app.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	goose.SetBaseFS(migrations.FS)
	if err := goose.SetDialect("sqlite3"); err != nil {
		log.Fatal(err)
	}
	if err := goose.UpContext(context.Background(), db, "."); err != nil {
		log.Fatal(err)
	}
}
```

migrationsパッケージをimportした時点で、同じパッケージのGoマイグレーションの`init`も走って登録されます。SQLとGoの両方が1つのバイナリで適用されました。なお、`create`や`fix`のようにファイルを書き換える操作は埋め込みFSではなくOSのファイルシステムに対して動きます。

### 複数Pod同時起動の競合とSessionLocker

レプリカが2つ以上あるアプリで起動時に`up`すると、同じマイグレーションを2つのプロセスが同時に実行しようとします。CLIの`goose`にも、上の`goose.UpContext`のようなグローバル関数にも、ロックの仕組みはありません。3秒かかるマイグレーション（`SELECT pg_sleep(3);`のあとにCREATE TABLE）を`goose.UpContext`で2プロセス同時に流すと、PostgreSQL 18.6で3回とも片方が失敗しました。

```
pod-a: OK   00001_slow.sql (3.03s)
pod-b: ERROR 00001_slow.sql: failed to run SQL migration: failed to execute SQL query "CREATE TABLE orders (id bigserial PRIMARY KEY, total integer NOT NULL);": ERROR: duplicate key value violates unique constraint "pg_class_relname_nsp_index" (SQLSTATE 23505)
```

空のDBで2つを同時に起動したときは、マイグレーションより手前の`goose_db_version`テーブルの作成で同じ種類の重複エラーが出て、片方が止まりました。初回のデプロイでも起きるということです。

ロックを使えるのは、`goose.NewProvider`で作るProvider API（v3.16.0で導入）だけです。PostgreSQLのアドバイザリロックを使う`lock.NewPostgresSessionLocker`を渡します。

```
sessionLocker, err := lock.NewPostgresSessionLocker()
if err != nil {
	log.Fatal(err)
}
provider, err := goose.NewProvider(
	goose.DialectPostgres,
	db,
	os.DirFS("migrations"),
	goose.WithSessionLocker(sessionLocker),
)
if err != nil {
	log.Fatal(err)
}
results, err := provider.Up(context.Background())
if err != nil {
	log.Fatal(err)
}
log.Printf("applied %d migration(s)", len(results))
```

同じ条件で2プロセスを同時に起動すると、先にロックを取ったpod-bが`applied 1 migration(s)`、待たされたpod-aがその3秒後に`applied 0 migration(s)`で正常終了しました。[lockパッケージ](https://pkg.go.dev/github.com/pressly/goose/v3/lock)の既定では、ロックを取れなかった側は5秒間隔で60回（合計5分）再試行してから諦めます。5分を超える長いマイグレーションがあるなら、オプションで延ばしてください。

importパスは`github.com/pressly/goose/v3/lock`です。MySQL／MariaDBには、v3.28.0で追加されたテーブル方式の`lock.NewMySQLTableLocker`を`goose.WithLocker`で渡します。`WithSessionLocker`と`WithLocker`は併用できません。

ロックを入れる前に、そもそもアプリ起動時に適用すべきかを判断してください。Kubernetesであれば、デプロイ前に1回だけ走るJobでマイグレーションを実行し、アプリ本体は`provider.HasPending`で未適用が無いことだけ確かめる構成の方が、ロック待ちでPodの起動が遅れる問題も起きません。起動時適用にロックを組み合わせるのは、Jobを用意できない小さな構成に限るのがよいでしょう。既存DBのスキーマ整理やDB製品の乗り換えを含む移行計画から相談したい場合は、一創の[データベース設計・移行支援](/service/system/database/)でも対応しています。

## golang-migrate・Atlasとの比較：Go向けマイグレーションツールの選び方

Goで使われるマイグレーションツールの主な選択肢は、goose・golang-migrate・Atlasの3つです。

| 観点            | goose         | golang-migrate          | Atlas                       |
| ------------- | ------------- | ----------------------- | --------------------------- |
| 最新版（2026年10月） | v3.29.0       | v4.20.1                 | v1.3.0                      |
| ファイル構成        | 1ファイルにUp／Down | .up.sqlと.down.sqlの2ファイル | スキーマ差分から生成                  |
| Goの関数で書く      | できる           | できない                    | できない                        |
| 失敗時の状態        | 対応する操作のみ巻き戻し  | dirtyになりforceが必要        | atlas\_schema\_revisionsに記録 |

golang-migrateは、途中で失敗したマイグレーションがあるとDBを「dirty」と記録し、`force VERSION`で手動で状態を直すまで先へ進めません（[golang-migrateのFAQ](https://github.com/golang-migrate/migrate/blob/master/FAQ.md)）。gooseは既定で各SQLファイルをトランザクション内で実行しますが、失敗時に巻き戻せるのは、そのDBでトランザクションの対象になる操作だけです。MySQLのCREATE TABLEなど、暗黙のコミットを伴うDDLは巻き戻せません。NO TRANSACTIONを付けたファイルも巻き戻しの対象外です。

選び方は次のとおりです。手書きのSQLに加えて既存データの加工をGoで書く場面があるなら、goose。SQLファイルだけで完結し、S3やGitHubなど多様な置き場所からマイグレーションを読みたいならgolang-migrate。スキーマ定義から差分SQLを自動生成したいならAtlasです。Atlasはgoose形式のマイグレーションディレクトリを読み書きできるので（[Atlasのgoose連携ガイド](https://atlasgo.io/guides/migration-tools/goose-import)）、生成はAtlas、適用はgooseという分担もできます。生成時は`atlas migrate diff`のディレクトリURLに`?format=goose`を指定します（[Atlasのmigrate diffの説明](https://atlasgo.io/versioned/diff)）。Java（Spring Boot）の標準的な選択肢は[Flywayとは？Spring Bootのマイグレーション設定・Callbacks・Spring Boot 4の変更点](/tech/details/2314/)、PHPのフレームワーク内蔵の仕組みは[Laravelマイグレーションの使い方｜php artisan migrateとrollback・freshの挙動を13.xで検証](/tech/details/7253/)で扱っています。

## よくある質問

### gooseとは何ですか？

Go言語で書かれたデータベースマイグレーションツール（pressly/goose）です。テーブルの作成やカラムの追加といったスキーマ変更をSQLファイルかGoの関数で書き、`goose up`で順番に適用し、`goose down`で戻します。適用履歴はDB内の`goose_db_version`テーブルに記録されます。CLIとしてもGoのライブラリとしても使え、PostgreSQL・MySQL・SQLiteなど12種類のドライバに対応しています。同じ名前のRust製負荷テストツールやAIエージェントとは別物です。

### goose migrationで「found 1 missing migrations」と出たらどうすればよいですか？

適用済みの最新バージョンより古い番号のファイルが後から追加されたときのエラーです。別ブランチで作ったタイムスタンプのファイルをマージすると起きます。その場限りで適用するなら`-allow-missing`を付けますが、実行順がファイル名の順と変わるため本番での常用は避けます。新規の運用では、開発中はタイムスタンプ、マージ時にCIで`goose fix`を実行して連番にする方法があります。ただし、既にDBへ適用したファイルは改名せず、既存の履歴と整合する番号を維持してください。

### goose upとup-by-one・up-toの違いは何ですか？

`goose up`は未適用のマイグレーションをすべて適用します。`up-by-one`は次の1つだけ、`up-to 20260901000000`は指定したバージョンまでを適用します。複数のマイグレーションを本番に出すとき、1つずつ適用して各段階でアプリの動作を確認したい場合は`up-by-one`を使います。適用結果は`goose status`で確認できます。

### go installで入れたgooseのバージョン確認と固定はどうしますか？

`goose --version`で`goose version: v3.29.0`のように表示されます。`goose version`（ハイフン無し）はDBの現在のマイグレーション番号を表示するコマンドなので混同しないでください。版を固定するには`go install github.com/pressly/goose/v3/cmd/goose@v3.29.0`のようにタグを指定します。v3.28.0以降はGo 1.26以上が必要で、Go 1.25.7以上の1.25系環境では`@v3.27.3`を指定します。

### golang-migrateとgooseはどちらを選ぶべきですか？

既存データの加工をGoのコードで書く必要があるならgooseです。golang-migrateはSQL（または各DBのクエリ）のファイルしか扱えません。失敗時の扱いも違い、golang-migrateはDBをdirty状態にして`force`での手動復旧を求めますが、gooseは既定でトランザクションを使います。ただし、NO TRANSACTIONを指定したファイルや、MySQLで暗黙のコミットを伴うDDLなどは巻き戻しを保証できません。SQLファイルだけで足り、S3などからマイグレーションを読みたい場合はgolang-migrateが合います。

## 関連記事

- [Go言語とは｜1.27系の導入からHTTP APIを動かすまでの実装入門](/tech/details/17483/)
- [GORM CreateInBatchesの一括挿入とトランザクション範囲｜v1.31.2の基本操作](/tech/details/3402/)
- [Flywayとは？Spring Bootのマイグレーション設定・Callbacks・Spring Boot 4の変更点](/tech/details/2314/)
- [Laravelマイグレーションの使い方｜php artisan migrateとrollback・freshの挙動を13.xで検証](/tech/details/7253/)
- [Goose（Rust）の使い方｜負荷テストの導入手順・コマンド引数・メトリクスの読み方](/column/details/3099/)

---

出典: [Gooseの使い方｜Go製マイグレーションツールv3.29のコマンドと本番運用](<https://www.issoh.co.jp/tech/details/3401/>)（株式会社一創）
