go-database について
go-databaseは、Goサービスのデータベースアクセスを設計する開発者向けのスキルです。クエリ、トランザクション、並行処理、性能、ツール、テストの指針をオンデマンドの資料とともに提供します。
主な機能
- 01接続プールの設定
- 02安全なクエリと行処理
- 03トランザクションとロックのパターン
- 04SQLツールとマイグレーション
- 05性能とテストの指針
活用シーン
- 01GoのDBアクセスコードのレビュー
- 02トランザクションと再試行ロジックの構築
- 03クエリや接続の問題の予防
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ eduardo-sl/ go-agent-skills --skill go-databaseインストール後、Claude Code で/go-databaseを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/locking-and-retries.mdperformance.mdquery-patterns.mdtesting.mdtooling.md詳細
Go データベースのパターン
Go サービスの複雑さの大半は、データベースアクセスに費やされます。 コネクション管理、トランザクション、クエリのパターンを適切に扱いましょう。
コミュニティの標準です。プロジェクトのスキルまたはエージェント設定ルールが
go-databaseを明示的に置き換える場合は、そちらが優先されます。
詳細なリファレンス資料は、必要に応じて読み込んでください。
references/query-patterns.md— クエリ、スキャン、行の完全なパターン、NULL の 処理、N+1 の回避、コネクションリークの例。references/tooling.md— リポジトリパターンの実装、sqlc の注釈付きクエリ、 マイグレーションツールとルール。references/locking-and-retries.md—FOR UPDATE/SKIP LOCKED、 楽観的バージョン管理、40001/40P01の再試行、制約エラー。references/performance.md— COPY とバッチ挿入、キーセットページネーション、 プールの統計情報、sql.Null[T]。references/testing.md— フェイクと実データベースの選択、 testcontainers のセットアップ、分離戦略。
以下の概要だけでは不十分な場合に限り、リファレンスファイルを読んでください。
1. コネクション管理
プールを明示的に設定してください。デフォルトではコネクション数に上限がありません。
func OpenDB(dsn string) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("open db: %w", err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(5 * time.Minute)
db.SetConnMaxIdleTime(1 * time.Minute)
if err := db.PingContext(context.Background()); err != nil {
return nil, fmt.Errorf("ping db: %w", err)
}
return db, nil
}
| 設定 | ガイドライン |
|---|---|
MaxOpenConns | DB の最大コネクション数/アプリインスタンス数に合わせます |
MaxIdleConns | MaxOpenConns の 40~50% |
ConnMaxLifetime | 5~10 分(ロードバランサーの背後で古くなったコネクションを防ぎます) |
ConnMaxIdleTime | 1~2 分 |
2. クエリのルール
- パラメーター化クエリのみを使用します — SQL 文字列の連結は例外なく インジェクションの脆弱性につながります。
- 必ずコンテキストを渡します —
*Contextバリアント (QueryContext、QueryRowContext、ExecContext)を使用して、クエリがキャンセルと タイムアウトに従うようにします。 - エラーチェックの直後に
defer rows.Close()を呼び出し、反復ループの後にrows.Err()を確認します。 sql.ErrNoRowsをerrors.Isで明示的に処理し、ErrUserNotFoundのような ドメインエラーにマッピングします。
var user User
err := db.QueryRowContext(ctx,
"SELECT id, name, email FROM users WHERE id = $1", id,
).Scan(&user.ID, &user.Name, &user.Email)
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrUserNotFound
}
if err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
複数行の反復パターン:references/query-patterns.md。
3. トランザクション
エラー時にロールバックを保証するヘルパーを使用します。
func WithTx(ctx context.Context, db *sql.DB, fn func(tx *sql.Tx) error) (err error) {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer func() {
if p := recover(); p != nil {
_ = tx.Rollback() // release the connection, then keep panicking
panic(p)
}
if err != nil {
if rbErr := tx.Rollback(); rbErr != nil && !errors.Is(rbErr, sql.ErrTxDone) {
err = errors.Join(err, fmt.Errorf("rollback: %w", rbErr))
}
}
}()
if err := fn(tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("commit tx: %w", err)
}
return nil
}
遅延ロールバックは fn 内でのパニックも処理します。これがない場合、コンテキストが
終了するまでトランザクションが開いたままとなり、コネクションもプールに戻りません。
errors.Join を使うと、両方のエラーを検査できます。
重要な操作では分離レベルを明示的に設定します。
sql.TxOptions{Isolation: sql.LevelSerializable}。
4. 並行制御とエラー
- 1 つのトランザクション内で読み取り後に書き込みを行う場合は
SELECT ... FOR UPDATEを使用します。ジョブ キューではFOR UPDATE SKIP LOCKEDで行を取得します。デッドロックを避けるため、行を 一貫した順序でロックします。 - ユーザー向けの編集には楽観的並行制御を使用します。
WHERE句にversion列を含め、 影響を受けた行が 0 件なら競合とします。 40001(シリアライゼーション失敗)と40P01(デッドロック)は再試行可能です。 ジッターを加えたバックオフと上限を設け、トランザクション関数全体を再試行します。 関数はトランザクション外に副作用を持たないようにしてください。- 制約違反をドメインエラーにマッピングします(
23505の一意制約違反 →ErrEmailTaken→ 409)。一意性の保証はデータベースに任せます。事前に確認してから 挿入すると競合が発生します。
コード:references/locking-and-retries.md。
5. 構成とツール
- リポジトリパターン: インターフェースは利用側で定義し、 具体的なデータベースアクセスで実装します。この境界でドライバーのエラーを ドメインエラーにマッピングします。
- sqlc: 生 SQL を使うプロジェクトでは優先して使用します。注釈付き SQL から 型安全な Go コードを生成し、ビルド時にクエリとスキーマの不一致を検出します。
- マイグレーション: ツール(
goose、golang-migrate、atlas)を使い、変更ごとに 1 つのマイグレーションを作成します。本番環境では前進のみとし、downSQL を用意して、 サーバー起動時ではなく別の手順として実行します。
実装と例:references/tooling.md。
6. よくある落とし穴
- NULL 列:
sql.NullString/sql.NullInt64またはポインターフィールド(*string、nil は SQL NULL)を使用します。NULL を通常の文字列にスキャンすると、実行時エラーになります。 - N+1 クエリ: クエリ結果をループ処理する中でクエリを実行することです。JOIN またはバッチクエリ(
WHERE id = ANY($1))に置き換えます。 - 接続リーク:
Queryとdefer rows.Close()の間に早期returnがあると、プールから接続がリークします。行を返さない文に対するdb.Queryも同様です。その場合はExecContextを使用します。
各落とし穴の具体例:references/query-patterns.md。
7. パフォーマンス
- 一括書き込みには、pgx の
CopyFrom、または数百行単位の複数行INSERTを使用します。行ごとにラウンドトリップする方法は避けてください。 OFFSETではなく、キーセットカーソル(WHERE (created_at, id) < ($1, $2))を使用してページネーションします。OFFSETはスキップした行をスキャンし、挿入があると位置がずれます。db.Stats()をエクスポートします。WaitCountの増加は、プールまたは接続を保持している何かがボトルネックであることを意味します。sql.Null[T](Go 1.22 以降)は、型ごとに用意されたNullString系の型に代わるものです。
例:references/performance.md。
8. テスト
サービスのロジックでは、利用側のリポジトリーインターフェースをフェイクにします。SQL はコンテナー内の実データベースに対してテストし(//go:build integration の背後で testcontainers を使用)、本番用のマイグレーションを実行します。テストは、テストごとのトランザクション、テンプレートデータベース、またはテーブルの切り詰めによって分離します。例:references/testing.md。
確認チェックリスト
- 接続プールに明示的な上限(
MaxOpenConns、MaxIdleConns、ライフタイム)を設定している - すべてのクエリでパラメーター化プレースホルダーを使用し、文字列連結は使用していない
- すべての
QueryContextの結果で、エラーチェック直後にdefer rows.Close()を実行している - 行の反復処理ループの後に
rows.Err()を確認している sql.ErrNoRowsをerrors.Isで明示的に処理している- トランザクションでは、エラー時のロールバックを保証するヘルパーを使用している
- すべてのデータベース呼び出しにコンテキストを引き継いでいる(
*Contextバリアント) - NULL 許容列には
sql.NullString/sql.NullInt64またはポインター型を使用している - N+1 クエリのパターンがない。JOIN またはバッチクエリを使用している
- マイグレーションはバージョン管理され、取り消し可能で、アプリの起動とは別に実行される
- 大規模なテーブルで
OFFSETページネーションを使用していない。一括書き込みはバッチ処理している - SQL はモックのクエリ文字列ではなく、実データベースに対してテストしている