mcpField
G
golang-dependency-injection

golang-dependency-injection

github.com/samber開発3.7k39.9k 回インストール5 日前に更新
リポジトリ

golang-dependency-injection について

Go の依存性注入を設計・リファクタリングするスキルです。手動配線と DI ライブラリの選択を支援し、テストしやすく疎結合なサービス構成を計画します。

主な機能

  1. 01設計・リファクタリングモード
  2. 02手動注入と DI ライブラリの比較
  3. 03コンストラクター注入の指針
  4. 04サブエージェントによるリファクタリング分析
  5. 05注入したモックによるテスト

活用シーン

  1. 01新しい Go サービスの依存関係の配線
  2. 02グローバル状態や密結合コードの改修
  3. 03規模に応じた DI 方式の選択
  4. 04モック依存を使ったサービスのテスト

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/samber/cc-skills-golang --skill golang-dependency-injection

インストール後、Claude Code で/golang-dependency-injectionを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

evals/
evals.json
references/
google-wire.md
manual-di.md
samber-do.md
uber-dig-fx.md

詳細

ペルソナ: あなたは Go ソフトウェアアーキテクトです。テストしやすく、疎結合な設計へチームを導きます。問題を解決する最もシンプルな DI 手法を選び、決して過剰に設計しません。

オーケストレーションモード: 大規模で密結合なコードベースを依存性注入に向けてリファクタリングする場合は、Refactor mode で説明されている 3 つのサブエージェント(グローバル変数/init の検出、具象依存関係のマッピング、サービスロケーターの検出)を並行して実行し、結果を 1 つの移行計画にまとめます。Claude Code では、ultracode を使ってマルチエージェントオーケストレーションを明示的に有効にします。

モード:

  • Design mode(新しいプロジェクト、新しいサービス、または既存の DI セットアップへのサービス追加):既存の依存関係グラフとライフサイクル要件を評価し、判断表に基づいて手動注入またはライブラリを推奨してから、配線コードを生成します。
  • Refactor mode(既存の密結合コード):最大 3 つのサブエージェントを並行して使用します。Agent 1 はグローバル変数と init() によるサービス設定を特定し、Agent 2 はインターフェースにすべき具象型の依存関係をマッピングし、Agent 3 はサービスロケーターのアンチパターン(引数として渡されるコンテナー)を特定します。その後、調査結果をまとめて移行計画を提案します。

コミュニティのデフォルト。 samber/cc-skills-golang@golang-dependency-injection スキルを明示的に置き換える企業スキルがある場合は、そちらを優先します。

Go における依存性注入

依存性注入(DI)とは、コンポーネント自身に依存関係を作成または検索させるのではなく、依存関係をコンポーネントに渡すことです。Go では、これによってテストしやすく疎結合なアプリケーションを構築できます。サービスが必要なものを宣言し、呼び出し元(またはコンテナー)がそれを提供します。

このスキルは網羅的なものではありません。DI ライブラリ(google/wire、uber-go/dig、uber-go/fx、samber/do)を使用する場合は、現在の API シグネチャとコード例について、各ライブラリの公式ドキュメントを参照してください。

インターフェースベース設計の基礎(インターフェースを受け取り、構造体を返す)については、samber/cc-skills-golang@golang-structs-interfaces スキルを参照してください。

ベストプラクティスの概要

  1. 依存関係はコンストラクター経由で注入しなければなりません(MUST)。サービスの設定にグローバル変数や init() を使ってはいけません(NEVER)。
  2. 小規模なプロジェクト(サービスが 10 個未満)では、手動のコンストラクター注入を使うべきです(SHOULD)。ライブラリは必要ありません。
  3. インターフェースは実装側ではなく、利用側で定義しなければなりません(MUST)。インターフェースを受け取り、構造体を返します。
  4. グローバルレジストリやパッケージレベルのサービスロケーターを使ってはいけません(NEVER)。
  5. DI コンテナーはコンポジションルート(main() またはアプリケーションの起動処理)にのみ存在しなければなりません(MUST)。コンテナーを依存関係として渡してはいけません(NEVER)。
  6. 遅延初期化を優先します。サービスは最初に要求されたときにのみ作成します。
  7. 状態を持つサービス(DB 接続、キャッシュ)にはシングルトンを、状態を持たないサービスにはトランジェントを使います。
  8. インターフェース境界でモックします。DI を使えば簡単に実現できます。
  9. 依存関係グラフは浅く保ちます。深いチェーンは設計上の問題を示しています。
  10. プロジェクトの規模とチームに適した DI ライブラリを選びます。以下の判断表を参照してください。

依存性注入が必要な理由

DI を使わない場合の問題DI による解決方法
関数が独自に依存関係を作成する依存関係を注入するため、実装を自由に差し替えられます
テストに実際のデータベースや API が必要になるテストではモック実装を渡します
あるコンポーネントを変更すると、他のコンポーネントが壊れるインターフェースによる疎結合で、コンポーネント同士が互いの内部実装を知る必要がありません
サービスがあちこちで初期化される集約されたコンテナーがライフサイクル(シングルトン、ファクトリー、遅延)を管理します
すべてのサービスが起動時に読み込まれる遅延読み込みにより、サービスは最初に要求されたときにのみ作成されます
グローバル状態と init() 関数が使われる起動時に明示的に配線するため、予測しやすくデバッグも容易です

DI は、HTTP サーバー、マイクロサービス、プラグインを備えた CLI ツールなど、多数のサービスが相互に接続されたアプリケーションで特に効果を発揮します。関数が 2~3 個の小さなスクリプトなら、手動で配線すれば十分です。過剰に設計しないでください。

手動によるコンストラクターインジェクション(ライブラリなし)

小規模なプロジェクトでは、依存関係をコンストラクター経由で渡します。アプリケーション全体の例については、手動 DI の例を参照してください。

// ✓ Good — explicit dependencies, testable
type UserService struct {
    db     UserStore
    mailer Mailer
    logger *slog.Logger
}

func NewUserService(db UserStore, mailer Mailer, logger *slog.Logger) *UserService {
    return &UserService{db: db, mailer: mailer, logger: logger}
}

// main.go — manual wiring
func main() {
    logger := slog.Default()
    db := postgres.NewUserStore(connStr)
    mailer := smtp.NewMailer(smtpAddr)
    userSvc := NewUserService(db, mailer, logger)
    orderSvc := NewOrderService(db, logger)
    api := NewAPI(userSvc, orderSvc, logger)
    api.ListenAndServe(":8080")
}
// ✗ Bad — hardcoded dependencies, untestable
type UserService struct {
    db *sql.DB
}

func NewUserService() *UserService {
    db, _ := sql.Open("postgres", os.Getenv("DATABASE_URL")) // hidden dependency
    return &UserService{db: db}
}

手動 DI では、次のような場合に対応が難しくなります。

  • 相互依存するサービスが 15 個以上ある
  • ライフサイクル管理(ヘルスチェック、グレースフルシャットダウン)が必要
  • 遅延初期化やスコープ付きコンテナーが必要
  • 配線の順序が壊れやすく、保守が難しくなる

DI ライブラリの比較

Go には、DI ライブラリの主なアプローチが 3 つあります。

判断基準の表
基準手動google/wireuber-go/dig + fxsamber/do
プロジェクトの規模小規模(サービス 10 個未満)中〜大規模大規模規模を問わない
型安全性コンパイル時コンパイル時(コード生成)実行時(リフレクション)コンパイル時(ジェネリクス)
コード生成なし必須(wire_gen.go)なしなし
リフレクションなしなしありなし
API スタイル該当なしプロバイダーセット + ビルドタグ構造体タグ + デコレーターシンプルなジェネリック関数
遅延読み込み手動該当なし(すべて即時)組み込み(fx)組み込み
シングルトン手動組み込み組み込み組み込み
一時インスタンス/ファクトリー手動手動組み込み組み込み
スコープ/モジュール手動プロバイダーセットモジュールシステム(fx)組み込み(階層型)
ヘルスチェック手動手動手動組み込みインターフェース
グレースフルシャットダウン手動手動組み込み(fx)組み込みインターフェース
コンテナーの複製該当なし該当なし該当なし組み込み
デバッグprint 文コンパイルエラーfx.Visualize()ExplainInjector()、ウェブインターフェース
Go のバージョン任意任意任意1.18 以降(ジェネリクス)
学習曲線なし中程度高い低い
簡単な比較:配線スタイル

同じ依存関係グラフ — Config -> Database -> UserStore -> UserService -> API — を、手動とコンテナーで配線します。違いは配線コードが表す内容です。手動では自分で管理する順序付きの呼び出しシーケンスですが、コンテナーではプロバイダーの集合を渡し、順序をコンテナーに任せます。

// Manual — you own the order; adding a dependency means editing every call site downstream
cfg := NewConfig()
db := NewDatabase(cfg)
store := NewUserStore(db)
svc := NewUserService(store)
api := NewAPI(svc)
api.Run()
// No shutdown hooks, health checks, or lazy loading — add them yourself

// Container (samber/do) — order is derived from the constructor signatures
i := do.New()
do.Provide(i, NewConfig)
do.Provide(i, NewDatabase)
do.Provide(i, NewUserStore)
do.Provide(i, NewUserService)
api := do.MustInvoke[*API](i)
api.Run()
defer i.Shutdown() // shutdown and health checks come from the container

google/wire と uber-go/fx は同じグラフを異なる方法で表現します。wire は wire.Build のプロバイダーリストから、上記の手動シーケンスをビルド時に生成します(プロバイダーが返す func() でクリーンアップし、ライフサイクルフックはありません)。一方、fx は fx.Provide でプロバイダーを登録し、実行時にリフレクションで解決して、OnStart/OnStop フックを利用します。それぞれの配線例については、google/wire、uber-go/dig + fx、samber/doを参照してください。

DI を使用したテスト

DI を使うと、実際の実装の代わりにモックを注入できるため、テストが簡単になります。

// Define a mock
type MockUserStore struct {
    users map[string]*User
}

func (m *MockUserStore) FindByID(ctx context.Context, id string) (*User, error) {
    u, ok := m.users[id]
    if !ok {
        return nil, ErrNotFound
    }
    return u, nil
}

// Test with manual injection
func TestUserService_GetUser(t *testing.T) {
    mock := &MockUserStore{
        users: map[string]*User{"1": {ID: "1", Name: "Alice"}},
    }
    svc := NewUserService(mock, nil, slog.Default())

    user, err := svc.GetUser(context.Background(), "1")
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    if user.Name != "Alice" {
        t.Errorf("got %q, want %q", user.Name, "Alice")
    }
}
samber/do を使用したテスト — 複製とオーバーライド

コンテナーを複製すると、必要なサービスだけをモックに置き換えられる、分離されたコピーを作成できます。

func TestUserService_WithDo(t *testing.T) {
    // Create a test injector with mock implementation
    testInjector := do.New()

    // Provide the mock UserStore interface
    do.OverrideValue[UserStore](testInjector, &MockUserStore{
        users: map[string]*User{"1": {ID: "1", Name: "Alice"}},
    })

    // Provide other real services as needed
    do.Provide[*slog.Logger](testInjector, func(i *do.Injector) (*slog.Logger, error) {
        return slog.Default(), nil
    })

    svc := do.MustInvoke[*UserService](testInjector)
    user, err := svc.GetUser(context.Background(), "1")
    // ... assertions
}

これは、ほとんどのサービスは実際のものを使いながら、特定の境界(データベース、外部 API、メール送信サービス)をモックにする必要がある統合テストで特に便利です。

DI ライブラリを導入するタイミング

兆候対応
サービスが 10 個未満で、依存関係がシンプル手動のコンストラクターインジェクションを続ける
サービスが 10〜20 個で、横断的関心事がいくつかあるDI ライブラリを検討する
サービスが 20 個以上で、ライフサイクル管理が必要強く推奨
ヘルスチェックやグレースフルシャットダウンが必要ライフサイクル管理が組み込まれたライブラリを使用する
チームが DI の概念に不慣れ手動で始め、段階的に移行する

よくある間違い

間違い修正方法
依存関係としてグローバル変数を使うコンストラクターまたは DI コンテナを通じて渡します
サービスのセットアップに init() を使うmain() またはコンテナで明示的に初期化します
具象型に依存する利用箇所の境界ではインターフェースを受け取ります
コンテナをあらゆる場所に渡す(サービスロケーター)コンテナではなく、個別の依存関係を注入します
依存関係の連鎖が深い(A->B->C->D->E)フラット化します。ほとんどのサービスは、リポジトリと設定に直接依存するようにします
リクエストごとに新しいコンテナを作成するアプリケーションごとにコンテナを1つ使い、リクエスト単位の分離にはスコープを使用します

相互参照

  • → samber/do の詳しい使用パターンについては、samber/cc-skills-golang@golang-samber-do スキルを参照してください
  • → インターフェースの設計と合成については、samber/cc-skills-golang@golang-structs-interfaces スキルを参照してください
  • → 依存性注入を使ったテストについては、samber/cc-skills-golang@golang-testing スキルを参照してください
  • → DI の初期化箇所については、samber/cc-skills-golang@golang-project-layout スキルを参照してください

参考資料

コメント