Upgrade to Pro — share decks privately, control downloads, hide ads and more …

GoでDIコンテナは必要か

Avatar for 22skyy 22skyy
February 22, 2026
2.7k

 GoでDIコンテナは必要か

Avatar for 22skyy

22skyy

February 22, 2026

Transcript

  1. 自己紹介 名前: 藤崎 裕樹 X: @_22skyy GitHub: 22skyy Go歴: 2年

    所属: SODA Inc. SNKRDUNK フリマアプリの開発
  2. DIとは — 依存を「外から渡す」設計パターン // ❌ 依存を内部で生成(差し替えしづらい) type UserServiceBad struct {

    repo *userRepoImpl // 具象型 } func NewUserServiceBad() *UserServiceBad { return &UserServiceBad{ repo: &userRepoImpl{}, // 内部で生成 } } // ✅ 依存を外から受け取る(DI) type UserServiceGood struct { repo UserRepository // interface } func NewUserServiceGood( repo UserRepository, // 外から渡す ) *UserServiceGood { return &UserServiceGood{repo: repo} } // DIならモック注入でテスト可能 func TestUserService(t *testing.T) { ctrl := gomock.NewController(t) mockRepo := mock.NewMockUserRepository(ctrl) mockRepo.EXPECT(). FindByID(gomock.Any(), 1). Return(&User{Name: "test"}, nil) svc := NewUserServiceGood( mockRepo, // ← モック注入 ) user, _ := svc.GetUser(1) assert.Equal(t, "test", user.Name) } メリット: テストしやすい(モック差し替え)/ 結合度が下がる(変更に強い)/ 依存関係が明示的(コードが追いやすい)
  3. 弊社でのDIパターン SNKRDUNKでの実践: コンストラクタDI + mockgenでモック生成 // repository.go - インターフェース定義 //go:generate

    mockgen -source=./repository.go -destination=./mock/repository.go type UserRepository interface { FindByID(ctx context.Context, id int) (*User, error) Save(ctx context.Context, user *User) error } // usecase.go - Usecase type Usecase struct { repo UserRepository } func NewUsecase(repo UserRepository) *Usecase { return &Usecase{repo: repo} } // usecase_test.go - テスト func TestUsecase(t *testing.T) { ctrl := gomock.NewController(t) mockRepo := mock.NewMockUserRepository(ctrl) mockRepo.EXPECT().FindByID(gomock.Any(), 1).Return(&User{}, nil) usecase := NewUsecase(mockRepo) // ... }
  4. 依存が増えると配線が大変 ... → 引数が増えてmainでの組み立てが大変に // Usecaseの依存が増える func NewUsecase( userRepo UserRepository,

    orderRepo OrderRepository, paymentRepo PaymentRepository, inventoryRepo InventoryRepository, notificationService NotificationService, ) *Usecase { ... } // 呼び出し側も大変 func main() { db := NewDatabase() userRepo := NewUserRepository(db) orderRepo := NewOrderRepository(db) paymentRepo := NewPaymentRepository(db) inventoryRepo := NewInventoryRepository(db) notificationService := NewNotificationService() usecase := NewUsecase( userRepo, orderRepo, paymentRepo, inventoryRepo, notificationService, ) }
  5. DIを楽にしたい 弊社:モノリス → モジュラーモノリス移行中 依存関係が複雑化 ・ 配線コードが増加が予想 → DIを楽にできたらいいな →

    google/wire が良さそうなので見てみよう wireの特徴 ✦ スター数が多い(2026/02時点 14.4k) ✦ オライリー本でよく紹介 ✦ Google製 ✦ コンパイル時コード生成
  6. google/wire — ビルド前にコード生成( go generate) → 配線コードを自動生成 ( go generate)

    → main.goはシンプルに呼ぶだけ // wire.go — 依存関係を宣言 //go:build wireinject func InitializeUsecase() *Usecase { wire.Build( NewDatabase, NewUserRepository, NewUsecase, ) return nil } // wire_gen.go — 自動生成される func InitializeUsecase() *Usecase { db := NewDatabase() userRepo := NewUserRepository(db) return NewUsecase(userRepo) } // main.go — 生成された関数を呼ぶだけ func main() { usecase := InitializeUsecase() usecase.Execute() } ※ wire.goはビルドには含めない [ディレクトリ構成例] project/ ├── wire.go # 依存関係を宣言 ├── wire_gen.go # 自動生成される ├── main.go └── internal/
  7. しかしwireはアーカイブされていた 2025年8月25日 アーカイブ "This project is no longer maintained" "Wire

    is considered feature complete" ✦ 機能は feature complete — 完成済み ✦ リポジトリは現在 read-only — 今後の変更は期待しづらい 後継ツールはありそう ✦ goforj/wire — fork後パフォーマンス改善 https://github.com/goforj/wire ✦ mazrean/kessoku — 並行化対応 https://github.com/mazrean/kessoku → これをきっかけに他の選択肢を検討してみよう → DIコンテナはどうだろう? → 完成とあるが、wireにはまだ改善余地があったよう (開発者体験に改善余地、初期化の並列化を表現しづらい ....etc) mazrean氏の発表を参照 KessokuのDIにおけるgoroutineスケジューリング / golang.tokyo #41 - Speaker Deck
  8. DIコンテナとは DI支援ツール( google/wire) DIコンテナ (uber-go/dig) 解決タイミング ビルド時(コード生成) 実行時(起動時に解決) 仕組み コード生成

    (runtime state / reflect なし) リフレクション エラー検出 生成時 + コンパイル時 実行時(起動時にfail-fast) 分類 DI支援ツール (compile-time DI) DIコンテナ (runtime DI) DIコンテナ = 実行時(主に起動時)に依存グラフを解決してオブジェクトを組み立てる仕組み ≒ ランタイムで依存解決する仕組み "container"の概念はMartin Fowler (2004) のブログやASP.NET Core公式等でも説明。 ※ DIコンテナの定義は文脈で揺れるため、本発表では上記で統一
  9. 他言語のDIコンテナ文化 と Go // Laravel(コンテナ経由で生成) class OrderService { public function

    __construct( private UserRepository $repo, private PaymentGateway $pay, ) {} // コンテナが依存を注入 } // 使う側 ← newしない $svc = app(OrderService::class); $svc->createOrder(1, $items); // テスト: bind で差し替え $this->app->bind( PaymentGateway::class, fn() => new MockPayment() ); // Spring Boot(コンテナ経由で生成) @Service public class OrderService { public OrderService( UserRepository repo, PaymentGateway pay ) {} } // 単一コンストラクタ→自動注入 // 使う側 ← newしない @Autowired OrderService svc; // 注入される // テスト: @MockBean で差し替え @MockBean PaymentGateway gw; // Go(自分で組み立てる) func NewOrderService( repo UserRepository, pay PaymentGateway, noti Notifier, ) *OrderService { ... } // 使う側 ← 自分でnewする svc := NewOrderService( userRepo, payment, notifier, ) // テストも自分で渡す svc := NewOrderService( mockRepo, mockPay, mockNotif, ) Laravel / Spring Boot: フレームワークにDIコンテナ内蔵 → 注入も差し替えも簡単 Go: 標準ではDIコンテナなし → main.go等で手書き( or 生成)が基本 ※PHP, Javaのコードは参考程度にお願いします
  10. フレームワーク文化の違い Laravel / Spring Boot等 ✦ フレームワークが DIコンテナを標準搭載(IoC Container /

    Service Container) ✦ コンストラクタ注入を標準ルートとして使える(自動で依存解決される) ✦ エコシステム全体(ライブラリ/サンプル/拡張)がコンテナ前提になりやすい ✦ 生成タイミングのデフォルト Laravel:解決(resolve)された時に生成 (基本は遅延。 singletonは初回resolve時に作って保持) Spring:singleton Beanは起動時生成がデフォルト (必要なら @Lazy で遅延可) ※ 実際の生成タイミングは設定 /スコープで変わる。ここでは「デフォルトの傾向」として整理 Go ✦ DIコンテナは標準的な前提ではない ✦ 標準ライブラリ + 必要なライブラリを組み合わせる文化 ✦ main などで手書き DI(明示的な組み立て)が基本 でも、本当にGoでDIコンテナが必要?(タイトル回収) → Redditでも議論されていた(必要ないかもという意見が多そう) Ref: What DI container do you use and why? : r/golang
  11. Goの言語仕様は DIに向いている Goは "interface + constructor" だけでDIが成立しやすい 暗黙的インターフェースの 3つの強み ❶

    implements宣言不要 シグネチャ一致で満たす → 差し替えが軽い ❷ 後付けで抽象化できる 型がメソッドセットを満たしていれば、後から interface側を導入しやすい ❸ 小さく切りやすい 最小のinterfaceで疎結合(テストが楽) Spring / Laravel: 注入の仕組み + 設定/スキャンが前提になりがち Go: 依存はコンストラクタ引数に並ぶ(コードで見える・追える) コード例 type UserRepository interface { FindByID(ctx context.Context, id string) (*User, error) } type GetUserUsecase struct{ repo UserRepository } func NewGetUserUsecase( repo UserRepository, ) *GetUserUsecase { return &GetUserUsecase{repo: repo} } // ↑ 実装側はimplementsキーワード不要で勝手に満たす
  12. io.Reader — 暗黙的インターフェースの教科書的例 → 暗黙的インターフェースだからこそ可能な設計 → 実装側はimplementsキーワードを書いていない / implements宣言なしでinterfaceを満たす //

    たった1メソッドのインターフェース type Reader interface { Read(p []byte) (n int, err error) } // これだけで以下が全て使える(implements宣言なし!) // - *os.File (ファイル) // - *bytes.Buffer (メモリ) // - net.Conn (ネットワーク) // - *gzip.Reader (圧縮) // テスト時は bytes.Buffer で簡単にモック化 func TestProcess(t *testing.T) { buf := bytes.NewBufferString("test data") result, err := Process(buf) // io.Readerを受け取る // assertion... } // ファイルI/Oのモック不要!
  13. 実務っぽい DI例: Usecase × Repository Usecaseがinterfaceに依存し、infraが暗黙的に満たす → DIコンテナなしで回る Usecaseはinterfaceに依存(実装詳細から分離) /

    実装(infra)はimplements宣言なしで満たす → 注入は NewX(repo)の引数だけ。 DIコンテナがないと無理になる場面は Goでは少ない 定義と実装 // usecase/get_user.go type UserRepository interface { FindByID(ctx context.Context, id string) (*User, error) } type GetUserUsecase struct{ repo UserRepository } func NewGetUserUsecase(repo UserRepository) *GetUserUsecase { return &GetUserUsecase{repo: repo} } // infra/mysql_user_repo.go type MySQLUserRepo struct{ db *sql.DB } func (r *MySQLUserRepo) FindByID( ctx context.Context, id string, ) (*User, error) { /* SELECT ... */ } 配線( main)とテスト // cmd/api/main.go(本番の配線) db := sql.Open("mysql", dsn) repo := infra.NewMySQLUserRepo(db) uc := usecase.NewGetUserUsecase(repo) h := handler.NewGetUserHandler(uc) // usecase/get_user_test.go(テスト) type InMemoryUserRepo struct { users map[string]*User } func (r *InMemoryUserRepo) FindByID( ctx context.Context, id string, ) (*User, error) { return r.users[id], nil } uc := NewGetUserUsecase(&InMemoryUserRepo{...})
  14. Goは手書きDIのコストが低い Goは"DIしやすい言語"なので、DIコンテナが必要になりにくい 言語 / フレームワーク コンテナ前提で登場しがちな要素 Spring Boot コンポーネントスキャン /

    Bean定義 /(必要に応じて)設定・プロファイル Laravel ServiceProvider / binding /(必要に応じて)設定・キャッシュ Go Constructor + Interface(+ 手書き wiring)— implements宣言もアノテー ションも不要
  15. GoにもDIコンテナライブラリがある ライブラリ ⭐ Stars 方式 特徴 備考 uber-go/dig ~4.4k リフレクション

    Provide/Invoke dig.As()でinterface Uber社内で実績豊富 uber-go/fx ~7.3k dig + フレームワーク dig + ライフサイクル管理 OnStart/OnStop digを内部に持ちライフサイ クルを提供 samber/do ~2.6k ジェネリクス 型安全なAPI Go 1.18+ / ライフサイクル対応 ジェネリクスライブラリ loで 有名なsamber氏作 NVIDIA/gontainer 64 リフレクション + ジェネリクス 依存ゼロ / no codegen Run()中心 / 逆順cleanup Go 1.21+ / v2 → Goでも自動解決ができる。使うべきかはトレードオフを見てから判断 ※ Stars数は2026年2月時点の概算。 gontainerはNVIDIA社製で比較的新しいプロジェクト 補足: fxはdigの上にライフサイクル管理( OnStart/OnStop)を追加したフレームワーク
  16. uber-go/dig の使い方 // 基本: Provide で登録、Invoke で解決 func BuildContainer() *dig.Container

    { c := dig.New() c.Provide(NewDatabase) c.Provide(NewUserRepository) c.Provide(NewOrderService) return c } func main() { c := BuildContainer() err := c.Invoke(func(svc *OrderService) { svc.Execute() // 依存は自動で組み立てられる }) if err != nil { log.Fatal(err) // 依存解決エラー } } // dig.As(): 具象→interfaceとして提供 c.Provide( NewDBUserRepo, dig.As(new(UserRepository)), ) c.Provide( NewStripePayment, dig.As(new(PaymentGateway)), ) // Invoke側はinterfaceで受け取れる c.Invoke(func(repo UserRepository) { }) [digのメリット] ✅ 配線(wiring)コードを減らせる(mainの初期化が短くなる) ✅ 型ベースで依存を自動解決できる( DIコンテナ的な体験) ✅ Uber製のOSSで、実運用前提の設計思想がある [ディレクトリ構成例 ] project/ ├── main.go # container.Invoke()を呼ぶ ├── container/ │ └── container.go # DIコンテナの初期化 └── internal/ ├── repository/ └── usecase/ ※ wireと違い、生成ファイル( wire_gen.go)はない
  17. トレードオフ① : 登録漏れが実行時エラー 実行結果: missing dependencies for function "main".NewUsecase: →

    Invoke() が error を返す → コンパイル時に検出できない 手書きDIならコンパイル時に検出できるエラーが、起動時まで分からなくなる // 依存を登録 container.Provide(NewDatabase) container.Provide(NewUserRepository) // container.Provide(NewOrderRepository) ← 登録忘れ! container.Provide(NewUsecase) // コンパイルは通る! err := container.Invoke(func(u *Usecase) { u.Execute() })
  18. トレードオフ③ : 依存の追跡が辛い バグ発生! OrderServiceに何が注入された? cmd + clickで飛んでもどの具象型かわからない ... //

    手書きDI → main.goを見れば全部分かる db := NewDatabase(cfg.DSN) userRepo := NewDBUserRepo(db) payment := NewStripePayment(cfg.APIKey) notifier := NewSlackNotifier(cfg.Webhook) svc := NewOrderService( ) // 実プロジェクト: Provide()が複数ファイルに分散するとさらに辛い // repository/provider.go c.Provide(NewDBUserRepo, dig.As(new(UserRepository))) c.Provide(NewDBOrderRepo, dig.As(new(OrderRepository))) // usecase/provider.go c.Provide(NewOrderService) // ← バグ発生。何が注入された? // Cmd+クリックしてもinterface定義に飛ぶだけで実装が分からない // → UserRepositoryの実装は? → repository/provider.goを探す // ※ dig.Visualizeで俯瞰はできる → Appendix A-6に例を掲載 手書きDIならmain.goの1ファイルを見れば 全部分かる
  19. どう判断すべきか プロジェクトに合うか吟味する ✦ 手書き DI ✅ 明示的、追跡しやすい ✅ IDEフレンドリー、コンパイル時にエラー検出 ❌

    依存が多い場合に生成が大変 ❌ mainに配線コードが増える ✦ DIコンテナ ✅ 配線を自動化 ✅ 大規模向け ❌ 暗黙的、実行時エラー ❌ 追跡困難、IDEのサポートが弱い 補足: コンテナはmainだけで使う。アプリの中で毎回コンテナから取り出すのは避ける ※ dig公式も "service locator としてアプリコードに露出させるのは Bad for" と明記
  20. 3方式の比較 手動DI コード生成 (wire等) DIコンテナ (dig/fx) コンパイル時 エラー検出 ✅ 完全

    ✅ 生成時に検出 ❌ なし IDE連携 ✅ 完全 ✅ 完全 ❌ 困難 配線コード量 多い 少ない 少ない 学習コスト 低い 中程度 高い ライフサイクル 自前実装 自前実装 ✅ fx対応
  21. まとめ:GoにおけるDIの選択基準 選択の目安 手書き DI コード生成( wire等) DIコンテナ( dig / fx)

    小〜中規模 配線コードを分離したい 起動時の配線を自動化 追跡性・IDE重視 型安全・明示性を維持 ライフサイクル管理(fx) コンパイル時にエラー検出 生成で明示的コードを保つ 他言語文化からの移行にも → まず手書き DI → つらくなった理由で段階的に選ぶ Goは暗黙的インターフェースで手書きDIのコストが低い → 他言語ほどコンテナは必須ではなさそう どれが正解ではなく、トレードオフを理解してチームで決める 弊社は RepositorySetパターンと手書き DI、mockgenで現状は十分回っている → Appendix A-2に例を掲載
  22. DI / DIコンテナの概念 • Martin Fowler — Inversion of Control

    Containers and the Dependency Injection pattern martinfowler.com/articles/injection.html • 同上 kakutani訳 kakutani.com/trans/fowler/injection.html • Microsoft — Dependency injection in ASP.NET Core learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection Goにおける DI • s-ota — GoのDIを理解する qiita.com/s-ota/items/584be39c7611c0d48b0c • cpp0302 — GoでDI(依存性注入)についてまとめ qiita.com/cpp0302/items/3c5254b840df6af24c10 • hanenao — GoのDIコンテナライブラリの比較 zenn.dev/hanenao/articles/eec5a5b3a28c9d • castingone_dev — GoでDIコンテナは必要か? zenn.dev/castingone_dev/articles/0b765ec8901d40 • hinom77 — GoのDIコンテナについて qiita.com/hinom77/items/1d7a30ba5444454a21a8 Reddit • r/golang — What DI container do you use and why? reddit.com/r/golang/comments/18qhc1t/ ライブラリ / ツール • awesome-go — Curated list of Go frameworks, libraries and software github.com/avelino/awesome-go • NVIDIA/gontainer — A DI container for Go github.com/NVIDIA/gontainer 発表資料 • mazrean — KessokuのDIにおけるgoroutineスケジューリング (Go Connect #6) speakerdeck.com/mazrean/go-connect-6 引用・参考資料
  23. A-1: DI支援ツールの選択肢 3つに分けると ... ✦ 手書きDI: 明示的・IDEフレンドリー・コンパイル時に壊れる ✦ コード生成(wire /

    kessoku等): 配線を生成して“明示性”も保つ ✦ DIコンテナ(dig / fx / do / gontainer等): 配線を自動化(ただし暗黙性↑) 選び方(本編の結論の再掲) ✦ 追跡性/IDE/安全性を優先 → 手書き or コード生成 ✦ 初期化が巨大で配線量が限界 → コンテナも検討(トレードオフ理解が前提)
  24. A-2: RepositorySetパターン 手書きDIを整理する小技 type RepositorySet struct { User UserRepository Order

    OrderRepository } func NewRepositorySet(db *Database) RepositorySet { return RepositorySet{ User: NewUserRepository(db), Order: NewOrderRepository(db), } } func NewUsecase(repos RepositorySet) *Usecase { return &Usecase{user: repos.User, order: repos.Order} } 使いどころ : 同じDB/Txにぶら下がる repoが増えて mainが膨れる時の整理 注意: Setが巨大化したら分割( “全部入り袋”になったら逆効果)
  25. A-3: interfaceの使いすぎに注意 性能と設計のバランス 起こりうるコスト ✦ escape次第で割り当て増(ヒープに逃げる) ✦ 動的ディスパッチの間接コスト(ホットパスだと効くことがある) 実務の指針 ✦

    interfaceは境界に置く(usecase↔repo、外部I/Fなど) ✦ ホットパスは計測してから( -gcflags=-m / pprof) ✦ テストのために何でも interface化はやりがち → 必要な場所だけに
  26. A-4: mazrean/kessokuの使い方 wire系コード生成 + 並列初期化の表現 何が嬉しい? ✦ wireライクな生成アプローチを維持しつつ ✦ 初期化の並列化を

    "宣言で表現"できる(DB/Cacheなど) var _ = kessoku.Inject[*App]("InitializeApp", kessoku.Async(kessoku.Provide(NewDatabase)), kessoku.Async(kessoku.Provide(NewCache)), kessoku.Provide(NewApp), ) ※ "配線は生成で明示的 "を保ったまま、起動時間の最適化に寄せられる ※ 詳細な使い方はリポジトリを参照 https://github.com/mazrean/kessoku
  27. A-5: goforj/wireについて google/wire互換を狙うmaintained fork ポイント ✦ //go:build wireinject を使うwire互換の書き味 ✦

    生成物はwire_gen.go(実行時依存なしの明示的コード) ✦ 互換性を重視しつつ改善(移行コストを押さえやすい) 最小イメージ( README準拠) //go:build wireinject func InitializeGreeter() *Greeter { wire.Build(GreeterSet) return nil } ※ 詳細な使い方はリポジトリを参照 https://github.com/goforj/wire
  28. A-6: dig.Visualize "俯瞰"の補助 できること ✦ 依存グラフをDOT形式で出力 → Graphviz等で図にできる(右図のような生成物 ) ✦

    オンボーディング / リファクタ / 循環調査で効く var buf bytes.Buffer _ = dig.Visualize(c, &buf) _ = os.WriteFile("deps.dot", buf.Bytes(), 0644) // dot -Tpng deps.dot > deps.png 注意 ✦ 「何が注入されたか」の追跡問題は残る(俯瞰はできるが、値までは追えない)
  29. A-7: uber-go/fxの使い方参考 dig + ライフサイクル管理 何が強い? ✦ OnStart/OnStop で起動・停止(graceful shutdown)を型で管理

    ✦ fx.Module で提供単位をまとめられる(規模が大きい時に効く) app := fx.New( fx.Provide(NewDB, NewHTTPServer), fx.Invoke(func(lc fx.Lifecycle, srv *http.Server) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { go srv.ListenAndServe(); return nil }, OnStop: func(ctx context.Context) error { return srv.Shutdown(ctx) }, }) }), ) app.Run() // シグナル受信でOnStopが呼ばれる ※ 詳細な使い方はリポジトリを参照 https://github.com/uber-go/fx
  30. A-8: NVIDIA/gontainerについて 新興: Run中心の体験( NVIDIA製) 特徴(思想として紹介) ✦ 依存ゼロ / コード生成なし(リフレクション

    + ジェネリクス) ✦ Run() 中心のAPI — 起動〜cleanup(逆順)まで一括管理 ✦ 新興でStarsもまだ小さめ(= 採用は慎重に) コード(概念図) g := gontainer.New() gontainer.Add(g, gontainer.NewFactory(NewDB)) gontainer.Add(g, gontainer.NewFactory(NewUserRepo)) gontainer.Add(g, gontainer.NewFactory(NewApp)) g.Run(context.Background()) // Run: 依存解決 → 起動 → シグナル待ち → cleanup(逆順) ※ 詳細な使い方はリポジトリを参照 https://github.com/NVIDIA/gontainer