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

GoでDIコンテナは必要か

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
Avatar for 22skyy 22skyy
February 22, 2026
2.3k

 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