🪢

【google/wire vs uber-go/dig】Golangにおける依存注入ライブラリの比較

2024/12/23に公開

はじめに

こちらはe-dash advent calendar 2024の23日目の記事です。


Golangには、依存注入を効率的に行うための代表的なライブラリとして、uber-go/diggoogle/wire の2つがあります。

本記事では、これら2つのライブラリの利用方法と特徴を比較し、それぞれの適用場面について解説します。導入に迷っている方に向けて参考になれば幸いです。

ライブラリを使用しない場合

まず、依存注入をライブラリを使わずに実装した場合を見てみましょう。

以下の例では、UserControllerに必要な依存を手動で注入しています。

type UserController struct {
	updateUserNameInteractor usecases.IUpdateUserNameInteractor
	userPresenter            presenters.IUserPresenter
	errorPresenter           presenters.IErrorPresenter
}

func NewUserController(
	updateUserNameInteractor usecases.IUpdateUserNameInteractor,
	userPresenter presenters.IUserPresenter,
	errorPresenter presenters.IErrorPresenter,
) *UserController {
	return &UserController{
		updateUserNameInteractor: updateUserNameInteractor,
		userPresenter:            userPresenter,
		errorPresenter:           errorPresenter,
	}
}

これを実際に使用する際には、以下のような依存解決コードを記述します。

func main() {
	client := NewClient()
	userController := NewUserController(
		updateUserNameInteractor: usecases.NewUpdateUserNameInteractor(
			userRepository: repositories.NewUserRepository(client: client),
		),
		userPresenter: presenters.NewUserPresenter(),
		errorPresenter: presenters.NewErrorPresenter(),
	)
}

この方法では、依存関係が複雑になるにつれて、解決のコードが肥大化し、保守が困難になります。

特に新しい依存関係が追加されるたびに、すべての関連部分に変更を加える必要があるため、新規開発フェーズでは負担が大きくなります。

google/wireの利用方法

https://github.com/google/wire

google/wire では、まずwire.Buildメソッドに依存解決に必要なConstructを登録します。

その後、wireコマンドを使用してコードを生成します。


使用例

//go:build wireinject

package infrastructure

import (
	"github.com/google/wire"
	// 他は省略
)

func InitializeUserController() *UserController {
	wire.Build(
		NewClient,
		controller.NewUserController,
		usecases.NewUpdateUserNameInteractor,
		repositories.NewUserRepository,
		presenters.NewUserPresenter,
		presenters.NewErrorPresenter,
	)
	return nil
}

ビルドタグ(//go:build wireinject)が設定されている場合には、 「go run -tags wireinject .」 のようにタグを指定した状態で実行しなければ無視されます。

そのため、タグ指定がない状態で実行すると生成コード側が参照されるようになります。


生成されたコードの例

// Code generated by Wire. DO NOT EDIT.

//go:generate go run -mod=mod github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject

package main

import (
	// 必要なパッケージ
)

// Injectors from wire.go:

func InitializeUserController() *UserController {
	iUserRepository := repositories.NewUserRepository()
	iUpdateUserNameInteractor := usecases.NewUpdateUserNameInteractor(iUserRepository)
	iUserPresenter := presenters.NewUserPresenter()
	iErrorPresenter := presenters.NewErrorPresenter()
	userController := controllers.NewUserController(iUpdateUserNameInteractor, iUserPresenter, iErrorPresenter)
	return userController
}

Golangの設計思想に基づくアプローチとしては、google/wireの方が適しています。

コード生成を通じて静的型付けと型安全性を活かしながら、高速に動作させることができるためです。


しかし、構造体や依存関係が変更された場合、wire.Buildに依存関係を追加した上で、コード生成をし直す必要があります。

この工程は頻繁に変更が入る場合には、少々手間に感じてしまうかもしれません。

uber-go/digの利用方法

https://github.com/uber-go/dig

uber-go/digでは、Provideメソッドを使用して依存解決に必要なConstructを登録します。

これにより、Invokeメソッドを実行した際に自動で依存解決を行ってくれます。


使用例

package infrastructure

import (
	"go.uber.org/dig"
	// 他は省略
)

func BuildContainer() (*dig.Container, error) {
	container := dig.New()
	dependencies := []interface{}{
		NewClient,
		controller.NewUserController,
		usecases.NewUpdateUserNameInteractor,
		repositories.NewUserRepository,
		presenters.NewUserPresenter,
		presenters.NewErrorPresenter,
	}

	for _, dependency := range dependencies {
		if err := container.Provide(dependency); err != nil {
			return nil, err
		}
	}

	return container
}

func main(){
	container := BuildContainer()

	var userController *controllers.UserController
	if err := container.Invoke(func(c *controllers.UserController) {
		userController = c
	}); err != nil {
		fmt.Println("Failed to resolving dependencies", zap.Error(err))
	}
}

ライブラリを使用しない解決方法よりもコード量が増えているように見えますが、新しく構造体を追加する際にProvideメソッドに登録するだけで済むため、管理が比較的容易になります。


一方で、digにはいくつかの課題もあります。

  1. 実行時エラー

    依存解決に問題がある場合、それが実行時まで発覚しません。エラーメッセージもやや分かりづらいため、初めて使用する際には苦労することがあるかもしれません。

  2. パフォーマンス

    内部でreflectを使用しているため、処理速度がやや低下します。

    しかし、依存解決は主にアプリケーションの起動時やルーター初期化時に行われるため、通常のAPI通信速度にはほとんど影響しません。

    ただし、特定のケース(Strategyパターンで依存解決が行われる場合等)では、パフォーマンスの影響を考慮する必要があります。

結論 どっちがいいの?

どちらが適しているかは、プロジェクトの特性や開発者の考え方によって異なります。

uber-go/digでは、依存解決コンテナが必ず実行される場合には、Invoke実行時にエラーが発生するため、問題に気づくことができます。

しかし、複数の依存解決コンテナを作成するような場合、それぞれのInvokeが実行されるまでエラーが発見できず、管理が煩雑になることがあります。

一方、google/wireは静的に依存解決を行い、コンパイル時にエラーを検出できるため、複数の依存コンテナを作成するような場合や長期的なメンテナンスを必要とするプロジェクトに適しています。


ただ個人的には、依存関係の変更後にコード生成を挟む必要がなく、すぐに実行できるuber-go/digの方が開発の手軽さを感じるため、好ましく思います。

Discussion