golang-samber-do について
Go 向けのジェネリクスベース依存性注入ツール samber/do の活用スキルです。コンポジションルートでのコンテナ管理、プロバイダーのエラー処理、スコープ、登録の整理を扱います。
主な機能
- 01ジェネリクスベースの依存性注入
- 02遅延・即時・都度生成・値サービス
- 03名前やインターフェースによる解決
- 04Package による登録の整理
- 05スコープとライフサイクルの指針
活用シーン
- 01Go アプリの依存性注入の設定
- 02アプリのモジュールごとのサービス登録
- 03プロバイダーからのインターフェース実装の解決
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ samber/ cc-skills-golang --skill golang-samber-doインストール後、Claude Code で/golang-samber-doを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/advanced.mdtesting.md詳細
ペルソナ: 依存性注入を設定する Go アーキテクトです。コンテナはコンポジションルートに置き、具象型ではなくインターフェースに依存し、プロバイダーのエラーを重大な失敗として扱います。
Go での依存性注入に samber/do を使う
Go 1.18 以降のジェネリクスに基づく、Go 用の型安全な依存性注入ツールキットです。
公式リソース:
このスキルは網羅的ではありません。詳細については、ライブラリのドキュメントとコード例を参照してください。
- Go パッケージのドキュメント、シンボル、バージョン、インポーター、既知の脆弱性については、Context7 よりも
samber/cc-skills-golang@golang-pkg-go-devスキル(godig)を優先して参照してください。 - 独自のコード内でこのライブラリの使い方(定義、呼び出し箇所、診断情報)を確認するには、
samber/cc-skills-golang@golang-goplsスキル(gopls)を参照してください。 - pkg.go.dev にインデックスされていないドキュメントについては、引き続き Context7 をフォールバックとして利用できます。
v2 をインストールしてください。v1 は後継のバージョンに置き換えられており、以下で説明するジェネリクスベースのコンテナ、スコープ、ライフサイクルフックがありません。そのため、v1 時代のガイダンスでは、このスキルに記載されているすべての API について誤解を招きます。
go get -u github.com/samber/do/v2
コアコンセプト
インジェクター(コンテナ)
import "github.com/samber/do/v2"
injector := do.New()
サービスの型
- 遅延(デフォルト):最初に要求されたときに作成されます
- 即時:コンテナの起動時にただちに作成されます
- 一時:要求のたびに新しいインスタンスが作成されます
- 値:事前に作成された値で、インスタンス化は行われません
プロバイダー関数
サービスはプロバイダー関数経由で登録する必要があります。
type Provider[T any] func(i Injector) (T, error)
基本的な使い方
1. サービスを定義して登録する
「インターフェースを受け取り、構造体を返す」に従います。
// Register a service (lazy by default)
do.Provide(injector, func(i do.Injector) (Database, error) {
return &PostgreSQLDatabase{connString: "postgres://..."}, nil
})
// Register a pre-created value
do.ProvideValue(injector, &Config{Port: 8080})
// Register a transient service (new instance each time)
do.ProvideTransient(injector, func(i do.Injector) (*Logger, error) {
return &Logger{}, nil
})
// Register an eager service (created immediately at startup)
do.ProvideValue(injector, &Config{Port: 8080})
2. サービスを呼び出す
コンテナへのアクセスはコンポジションルートに限定する必要があります。
// Invoke with error handling — reserve for call sites outside the DI graph
// (e.g. an HTTP handler that must degrade gracefully instead of crashing)
db, err := do.Invoke[Database](injector)
// MustInvoke panics on error — preferred in providers, recovered by do.Invoke on the parent call
db := do.MustInvoke[Database](injector)
プロバイダー関数内では、エラーを返すバリアントではなく、常に do.MustInvoke(または MustInvokeAs/MustInvokeNamed/MustInvokeStruct)を使用してください。
- プロバイダーはすでに
(T, error)を返すため、依存関係のエラーをdo.Invokeで伝播すると、呼び出しのたびに余分なif err != nil { return nil, err }が必要になります。 do.MustInvokeは代わりにパニックを発生させますが、samber/do はそのパニックを外側のInvoke呼び出しで正しく捕捉して復旧し、通常のエラーに変換します。この復旧処理は呼び出し元のコードではなくライブラリ自体の内部で行われるため、プロバイダー内でMustInvokeを安全に使用できます。- 失敗は引き続きコンポジションルートでエラーとして表面化しますが、各プロバイダーで手動の定型コードを記述する必要はありません。
3. サービスの依存関係
func NewUserService(i do.Injector) (UserService, error) {
db := do.MustInvoke[Database](i)
cache := do.MustInvoke[Cache](i)
return &userService{db: db, cache: cache}, nil
}
do.Provide(injector, NewUserService)
4. 暗黙のエイリアス(推奨)
具象型を登録し、明示的なエイリアスを設定せずにインターフェースとして呼び出します。
// Register concrete type
do.Provide(injector, func(i do.Injector) (*PostgreSQLDatabase, error) {
return &PostgreSQLDatabase{}, nil
})
// Invoke directly as interface (implicit aliasing)
db := do.MustInvokeAs[Database](injector)
5. 名前付きサービス
同じ型のサービスを複数登録します。
do.ProvideNamed(injector, "primary-db", func(i do.Injector) (*Database, error) {
return &Database{URL: "postgres://primary..."}, nil
})
mainDB := do.MustInvokeNamed[*Database](injector, "primary-db")
パッケージ構成
do.Package() を使用して、モジュールごとにサービス登録を整理します。
// infrastructure/package.go
var Package = do.Package(
do.Lazy(func(i do.Injector) (*postgres.DB, error) {
cfg := do.MustInvoke[*Config](i)
return postgres.Connect(cfg.DatabaseURL)
}),
do.Lazy(func(i do.Injector) (*redis.Client, error) {
cfg := do.MustInvoke[*Config](i)
return redis.NewClient(cfg.RedisURL), nil
}),
)
// main.go
injector := do.New(infrastructure.Package, service.Package)
アプリケーション全体のセットアップ
func main() {
injector := do.New(
infrastructure.Package,
repository.Package,
service.Package,
transport.Package,
)
server := do.MustInvoke[*http.Server](injector)
go server.ListenAndServe()
_ = injector.ShutdownOnSignalsWithContext(context.Background(), os.Interrupt)
}
ベストプラクティス
- 具体的な型ではなくインターフェースに依存します。そうすることで、本番コードに手を加えずに、テストで実装を差し替えられます。
- 各サービスの役割は1つにします。複数の責務を持つサービスは、テストも差し替えも難しくなります。
- 依存関係のツリーは浅く保ちます。3〜4階層を超えるチェーンは、初期化順序が不安定になり、エラーの追跡も難しくなります。
- プロバイダー関数でエラーを処理します。エラーを黙って無視するプロバイダーは、壊れたサービスを作り、後になって予期しない箇所でクラッシュを引き起こします。
- スコープを使って、サービスをライフサイクルごとに整理します。リクエストスコープのサービスはリークを防ぎ、グローバルサービスは冗長な初期化を防ぎます。
- プロバイダー関数の内部では
do.Invoke*ではなくdo.MustInvoke*を使います。samber/do は外側のInvoke呼び出しでパニックを適切に捕捉して復旧し、返されたエラーに変換するため、プロバイダーの内部でも安全に使用でき、定型コードなしで同じエラー伝播を実現できます。
スコープ、ライフサイクル管理、構造体へのインジェクション、デバッグについては、高度な使い方を参照してください。
テストのパターン(クローン、オーバーライド、モック)については、テストを参照してください。
クイックリファレンス
登録
| 関数 | 用途 |
|---|---|
do.Provide[T]() | 遅延サービスを登録(デフォルト) |
do.ProvideNamed[T]() | 名前付き遅延サービスを登録 |
do.ProvideValue[T]() | 作成済みの値を登録 |
do.ProvideNamedValue[T]() | 名前付きの値を登録 |
do.ProvideTransient[T]() | 呼び出しごとに新しいインスタンスを登録 |
do.ProvideNamedTransient[T]() | 名前付きの一時サービスを登録 |
do.Package() | サービスの登録をグループ化 |
呼び出し
| 関数 | 用途 |
|---|---|
do.Invoke[T]() | サービスを取得(エラーを返す) |
do.InvokeNamed[T]() | 名前付きサービスを取得 |
do.InvokeAs[T]() | インターフェースに一致する最初のサービスを取得 |
do.InvokeStruct[T]() | タグを使って構造体のフィールドにインジェクション |
do.MustInvoke[T]() | サービスを取得(エラー時にパニック) |
do.MustInvokeNamed[T]() | 名前付きサービスを取得(エラー時にパニック) |
do.MustInvokeAs[T]() | インターフェースでサービスを取得(エラー時にパニック) |
do.MustInvokeStruct[T]() | 構造体にインジェクション(エラー時にパニック) |
関連項目
- → DI の概念、比較、DI ライブラリを導入するタイミングについては、
samber/cc-skills-golang@golang-dependency-injectionスキルを参照してください - → インターフェース設計パターンについては、
samber/cc-skills-golang@golang-structs-interfacesスキルを参照してください - → 一般的なテストパターンについては、
samber/cc-skills-golang@golang-testingスキルを参照してください