mcpField
G
go-dependency-injection

go-dependency-injection

github.com/eduardo-sl開発8396 回インストール2 週間前に更新
リポジトリ

go-dependency-injection について

go-dependency-injection は、依存関係の明示的な受け渡しとコンポジションルートでの組み立てを解説する Go 開発スキルです。手動配線を基本とし、DI ツールを使う条件や各方式の違いも扱います。

主な機能

  1. 01コンストラクターによる依存性注入
  2. 02main または run への配線集約
  3. 03可変グローバル状態の回避
  4. 04小さな接点への関数注入
  5. 05DI ツールの選択指針

活用シーン

  1. 01サービス依存関係の設計
  2. 02パッケージ単位のシングルトン解消
  3. 03フェイクを使ったコンポーネントテスト
  4. 04Go の依存性注入方式の選定

インストール

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

$npx skills add https://github.com/eduardo-sl/go-agent-skills --skill go-dependency-injection

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

詳細

Go の依存性注入

Go における DI はパターンであり、フレームワークではありません。依存関係はコンストラクターに渡し、main ですべてを明示的に接続します。手動での接続が測定可能なほど負担になった場合にのみ、フレームワークを導入します。

コミュニティの標準です。go-dependency-injection に明示的に優先するプロジェクトのスキルまたはエージェント設定ルールがある場合は、そちらを優先します。

1. コンストラクターインジェクション — デフォルト

// ✅ Good — dependencies are explicit parameters
type OrderService struct {
    repo     OrderRepository
    payments PaymentGateway
    logger   *slog.Logger
}

func NewOrderService(repo OrderRepository, payments PaymentGateway, logger *slog.Logger) *OrderService {
    return &OrderService{repo: repo, payments: payments, logger: logger}
}

// ❌ Bad — hidden dependencies reached through globals
func (s *OrderService) Place(ctx context.Context, o Order) error {
    db := database.Get()        // global singleton
    log.Printf("placing order") // global logger
    // untestable without touching process-wide state
}

ルール:

  • サービスが呼び出す依存関係はインターフェースとして受け取り、コンストラクターからは具象型を返します。
  • すべての依存関係をシグネチャに明示します。リストが長く感じられるなら、その型も役割が多すぎます(分割してください)。依存関係を隠してリストを短くしてはいけません。
  • コンストラクターで必須の依存関係を検証し、エラーを返します(または、logger = slog.Default() のように nil 安全なデフォルトを受け入れます)。

2. コンポジションルート

すべての接続処理は 1 か所、つまり main(または main が呼び出す run 関数)にまとめます。構築順序は依存関係の順序と一致し、コンパイラーによって検証されます。

func run(ctx context.Context, cfg Config) error {
    db, err := store.Open(ctx, cfg.DatabaseURL)
    if err != nil {
        return fmt.Errorf("open db: %w", err)
    }
    defer db.Close()

    orderRepo := store.NewOrderRepo(db)
    payments := stripe.NewGateway(cfg.StripeKey)
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    orders := service.NewOrderService(orderRepo, payments, logger)
    server := handler.NewServer(cfg.Addr, orders)

    return server.ListenAndServe(ctx)
}
  • パッケージが独自に依存関係を構築することはありません。依存関係は受け取ります。
  • init() による接続や、パッケージレベルの var DB *sql.DB は使いません。
  • 異なる接続処理が必要な 2 つのバイナリには、同じコンポーネントを使って 2 つの main を用意します。

3. グローバル状態の排除

// ❌ Before — package-level singleton
var defaultClient *api.Client

func Fetch(id string) (*Item, error) {
    return defaultClient.Get(id)
}

// ✅ After — the dependency moves into a struct
type Fetcher struct {
    client *api.Client
}

func NewFetcher(c *api.Client) *Fetcher { return &Fetcher{client: c} }

func (f *Fetcher) Fetch(id string) (*Item, error) {
    return f.client.Get(id)
}

レガシーコードベースの移行手順:構造体を導入し、非推奨のパッケージレベルラッパーを残して、main で構築した 1 つのインスタンスに処理を委譲します。呼び出し元を順次移行し、ラッパーを削除します。

許容されるパッケージレベルの状態:純粋な定数、コンパイル済み正規表現、設定を保持しない sync.Once で保護されたプロセスシングルトン。

4. 小さな接点には関数依存を使う

1 つの関数だけのためにインターフェースを作るのは過剰です。関数を注入します。

type Service struct {
    now     func() time.Time
    genID   func() string
    publish func(ctx context.Context, e Event) error
}

// Production: Service{now: time.Now, genID: uuid.NewString, publish: bus.Publish}
// Test:       Service{now: fixedTime, genID: constID, publish: capture}

5. フレームワークの複雑さに見合う場合

手動での接続処理は、想定以上に規模を拡大できます。100 行の run 関数でも読みやすく、コンパイラーによる検証も受けられます。接続対象が数百のコンポーネントに及ぶ場合や、多数のチームで 1 つのバイナリを共有する場合は、ツールを検討してください。

ツールモデルトレードオフ
google/wireコンパイル時のコード生成接続処理は通常の Go のままで、コンパイラーによる検証も受けられます。コード生成の手順が追加されます
uber-go/fxランタイムコンテナー + ライフサイクルアプリケーションのライフサイクル(開始/停止フック)が管理されます。エラーは実行時に表面化し、スタックトレースにはマジックが現れます
uber-go/digランタイムコンテナー(fx のコア)同じ実行時のトレードオフがありますが、ライフサイクル層はありません

判断基準:手動での接続を優先します。コード生成が必要になった場合は、wire を優先してください(起動時の失敗よりコンパイル時の失敗が望ましいためです)。ライフサイクル管理も必要で、チームがランタイムコンテナーを受け入れる場合にのみ fx を導入します。

モデルを混在させてはいけません。コンポジションルートは 1 つ、仕組みも 1 つにします。

6. Wire の例(採用する場合)

//go:build wireinject

func InitializeServer(cfg Config) (*handler.Server, error) {
    wire.Build(
        store.Open,
        store.NewOrderRepo,
        stripe.NewGateway,
        service.NewOrderService,
        handler.NewServer,
    )
    return nil, nil // replaced by generated code
}

wire は順序付けされたコンストラクター呼び出しを生成します。生成ファイルはコミットし、手書きのコードと同様にレビューします。

検証チェックリスト

  1. すべてのサービス/ハンドラーが、コンストラクターパラメーターを通じて依存関係を受け取る
  2. パッケージレベルの可変シングルトン(var DB、var logger、Get() アクセサー)がない
  3. すべての接続処理が main/run に集約されており、init() で構築していない
  4. 依存関係はインターフェース(または関数)として受け取り、具象型を返す
  5. コンストラクターが必須の依存関係を検証する
  6. フェイクを渡してコンポーネントをテストでき、テストでプロセス全体のグローバルなセットアップを必要としない
  7. DI ツールを使う場合:コンポジションルートでのみ、ちょうど 1 つ使う
  8. go build ./... が成功する。接続処理のエラーはコンパイル時に表面化する

コメント