go-dependency-injection について
go-dependency-injection は、依存関係の明示的な受け渡しとコンポジションルートでの組み立てを解説する Go 開発スキルです。手動配線を基本とし、DI ツールを使う条件や各方式の違いも扱います。
主な機能
- 01コンストラクターによる依存性注入
- 02main または run への配線集約
- 03可変グローバル状態の回避
- 04小さな接点への関数注入
- 05DI ツールの選択指針
活用シーン
- 01サービス依存関係の設計
- 02パッケージ単位のシングルトン解消
- 03フェイクを使ったコンポーネントテスト
- 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 は順序付けされたコンストラクター呼び出しを生成します。生成ファイルはコミットし、手書きのコードと同様にレビューします。
検証チェックリスト
- すべてのサービス/ハンドラーが、コンストラクターパラメーターを通じて依存関係を受け取る
- パッケージレベルの可変シングルトン(
var DB、var logger、Get()アクセサー)がない - すべての接続処理が main/run に集約されており、
init()で構築していない - 依存関係はインターフェース(または関数)として受け取り、具象型を返す
- コンストラクターが必須の依存関係を検証する
- フェイクを渡してコンポーネントをテストでき、テストでプロセス全体のグローバルなセットアップを必要としない
- DI ツールを使う場合:コンポジションルートでのみ、ちょうど 1 つ使う
go build ./...が成功する。接続処理のエラーはコンパイル時に表面化する