フィールドタグだけで完結する Go の DI ツール「injector」- uber-go/dig との比較 -
フィールドタグだけで完結する Go の DI ツール injector というものを作った。
既存の DI ライブラリとして使われることの多い、uber-go/dig と比較してみる。
google/wire との比較は以下
uber-go/fx との比較は以下
uber-go/dig と injector をコード量で比較してみる
Go の DI ライブラリとして uber-go/dig は、軽量で柔軟なランタイム DI コンテナとしてよく使われている。
一方 injector は、DI をランタイムではなく 静的解析 + コード生成で解決するアプローチを取る。
この記事では、
「開発者が実際に書き、維持する DI コード量」
という観点で、dig と injector を比較する。
※ 実行時の内部コード量ではなく、人間が読む・直す・レビューするコードに注目する。
想定する構成
比較をフェアにするため、これまでと同様に、よくあるバックエンド構成を想定する。
- Config
- Database(初期化で error が起き得る)
- Repository
- Service
- Handler
依存関係は次の通り。
Config → Database → Repository → Service → Handler
dig の場合
dig はコンテナを生成し、provider を登録し、最後に Invoke で必要な依存を取り出す。
Provider 定義
func NewConfig() *Config
func NewDatabase(cfg *Config) (*Database, error)
func NewUserRepository(db *Database) *UserRepository
func NewUserService(repo *UserRepository) *UserService
func NewUserHandler(svc *UserService) *UserHandler
Container 構築と Provide
func buildContainer() (*dig.Container, error) {
c := dig.New()
if err := c.Provide(NewConfig); err != nil {
return nil, err
}
if err := c.Provide(NewDatabase); err != nil {
return nil, err
}
if err := c.Provide(NewUserRepository); err != nil {
return nil, err
}
if err := c.Provide(NewUserService); err != nil {
return nil, err
}
if err := c.Provide(NewUserHandler); err != nil {
return nil, err
}
return c, nil
}
起動コード(Invoke)
func main() {
c, err := buildContainer()
if err != nil {
log.Fatal(err)
}
if err := c.Invoke(func(h *UserHandler) {
// start server
_ = h
}); err != nil {
log.Fatal(err)
}
}
dig で人間が書く DI コード
dig.New()-
Provideの列挙(error handling 付き) -
Invokeの起動点
小規模でも 30〜50 行程度は発生する。
provider が増えるほど Provide の列挙と error handling が増えていく。
injector の場合
injector は、Container に inject を付けて「露出させたい依存」を宣言するだけでよい。
Container 定義
type Container struct {
Handler *UserHandler `inject:""`
}
生成されるコード(要点)
func NewContainer() (*Container, error) {
cfg := NewConfig()
db, err := NewDatabase(cfg)
if err != nil {
return nil, err
}
repo := NewUserRepository(db)
svc := NewUserService(repo)
h := NewUserHandler(svc)
return &Container{Handler: h}, nil
}
Must モード(エントリポイント向け)
injector generate --must ./...
func MustNewContainer() *Container {
c, err := NewContainer()
if err != nil {
panic(err)
}
return c
}
--on-error=fatal を指定すれば log.Fatal に切り替えられる。
コード量の比較
| 項目 | dig | injector |
|---|---|---|
| DI 初期化コード |
Provide + Invoke
|
Container 定義のみ |
| provider 登録 | 必須(列挙) | 不要 |
| 書く行数(小規模) | 30〜50 行 | 1〜3 行 |
| エラーの扱い | 毎回明示 |
New / MustNew を選べる |
| 依存解決タイミング | ランタイム | 静的解析 + 生成 |
コード量の差は、ほぼそのまま
-
Provideの列挙 - error handling
-
Invokeの起動点
の差になる。
dig が向いているケース
- 実行時に provider を差し替えたい
- plugin 的な構成がある
- コンテナを動的に組み替えたい
- DI を「ライブラリ」として軽く使いたい
injector が向いているケース
- 依存関係を コンパイル前に確定させたい
- DI 記述を最小限にしたい
- 起動失敗を build-time / generate-time で検出したい
- Container を明示的な API として扱いたい
まとめ
dig は軽量で柔軟なランタイム DI を提供するが、
- provider 登録の列挙
- error handling
- 起動コード
といった 人間が管理する DI コードが必ず発生する。
injector は、
- Container に
injectを書く - provider は普通の関数を書く
という最小構成で、DI の記述量がほとんど増えない。
**「配線コードを書きたくない」「型に任せたい」**なら、injector は dig に対する明確な代替になり得る。
Discussion