26
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

NRI OpenStandia Advent Calendar 2022

Day 24

GoのDependency Injectionライブラリuber-go/dig, google/wireを比較する

Last updated at Posted at 2022-12-24

はじめに

普段Java、Spring Frameworkを使って開発していると、Goで開発する場合の依存解決には苦労することがあるように思います。

Dependency Injection(以下DI)をやるとして、階層の深い依存関係のある構造体を初期化しようとすると、あれもこれもとコンストラクタを呼ぶことになります。また、往々にしてそういった構造体は1つではないので、どでかい初期化処理が生まれることになります。

そこで、本記事では、GoにいくつかあるDependency Injectionをサポートするライブラリについて調べていきます。

Goの依存解決の課題は何か

作成するアプリケーションや、開発体制が大きくなればなるほど、以下のような課題が出てきます。

  1. コンストラクタをたくさん呼び出して、やっとこさ初期化できるインスタンス
  2. 初期化の定義が大集合しがち。複数人が同じファイルをあれやこれやと編集することになる。
  3. 構造体が実装しているインタフェースを明示的に記載しないので、後から読むと困る場合が多い。

DIライブラリを調べる

2022/12/23時点でawesome-goに記載のあるDIライブラリは以下の通りです。

ライブラリ fork star License
magic003/alice 4 51 MIT License
goava/di 8 168 MIT License
uber-go/dig 187 2.9k MIT License
i-love-flamingo/dingo 9 153 MIT License
samber/do 23 727 MIT License
uber-go/fx 229 3.3k MIT License
vardius/gocontainer 2 18 MIT License
goioc/di 12 227 MIT License
golobby/container 28 406 MIT License
google/wire 533 9.7k Apache-2.0 license
HnH/di 4 6 MIT License
go-kata/kinit 1 9 MIT License
logrange/linker 6 36 Apache-2.0 license
muir/nject 1 23 MIT License
Fs02/wire 8 37 MIT License

最もstar数の多いgoogle/wireで、9.7kということで.あまり、あまりGoの世界ではDIライブラリの利用は浸透していない印象を受けます。
手続き型言語であるGoで依存関係を定義する煩雑さを厭うのはおかしいのかもしれませんが、気を取り直して本記事では、star数の多いuber-go/diggoogle/wireについてみていくこととします。(uber-go/fxuber-go/digをベースにしたアプリケーションフレームワークなので割愛)

ライブラリ概要

uber-go/dig

DIコンテナを提供するライブラリ。
コンストラクタをコンテナに登録することで、必要な型のインスタンスを提供する機能を持つ。
必要なそれぞれのインスタンスを1度だけ作成して使いまわす。

google/wire

依存性を注入するコードの生成ツール。
コンストラクタを設定することで、必要な型のインスタンスを返却する関数を生成する機能を持つ。
初期化したインスタンスを管理するような機能はない。

従って、モックを注入して扱うなどする場合は工夫が必要。
GoのDIライブラリ google/wire でモックを使う場合のベストプラクティス」が参考になりました。

機能を比較する

双方のドキュメントを確認して機能的な差分をまとめてみました。

項目 uber-go/dig google/wire
DIの手段 コンストラクタ コンストラクタ
インスタンスの提供方法 DIコンテナ 生成した関数
提供インスタンスのスコープ singleton prototype
依存解決のタイミング 実行時 コード生成時
依存グラフ 暗黙的 明示的
interfaceを実装する構造体の解決 明示的に登録 明示的に登録
interface型インスタンスを返却できる 可能 可能
struct型インスタンスを返却できる 可能 可能
登録するコンストラクタに複数の引数、戻り値を指定できる 可能 可能
インスタンス取得時にコンストラクタの引数の値を指定できる 不可
コンストラクタの登録時に値をセットしてやる必要がある
可能
生成した関数の引数で指定できる
同一の型を返却するコンストラクタを同時に登録できる・使い分けられる 可能
ラベルを与えることで同一の型を使い分ける
不可
型を分ける。関数生成を分割するなどの対応が必要
依存上不要なコンストラクタのパッケージの扱い コンテナに登録したパッケージは依存グラフの登場有無に関わらずビルドに含める インスタンスに必要なパッケージのみに依存した関数を使う

できないことは、DIに対するアプローチによって不要と判断している機能と判断できそうですが、どちらのライブラリにおいても特色がありそうです。次からは、実装を確認して特徴をつかんでいきます。

実装を比較する

google/wireのサンプルを参考に下記に示すような構成において、Eventインタフェース型でインスタンスを受け取り、Startメソッドの実行を目指します。

シンプルで依存関係も複雑ではないですが、ポイントは以下の通りです。

  • EventインタフェースをGreeterEventが実装しています。
  • GreeterEventGreeterインタフェースに依存しています。
  • GreeterインタフェースをEnglishGreeterが実装しています。
  • Greeterは作成されたタイミングでたまに不機嫌(Grumpy)です。不機嫌である場合、GreeterEventのコンストラクタ処理を失敗させます。
  • MessageString型をラップしています。

file.png
※UML図はkazukousen/goumlにて生成しています。

折りたたんでいますが、UML図の実装はこちら

import (
	"errors"
	"fmt"
	"os"
	"time"
)

func NewMessage(phrase string) Message {
	return Message(phrase)
}

type Message string

type Greeter interface {
	Greet() Message
	Grumpy() bool
}

func NewEnglishGreeter(m Message) EnglishGreeter {
	return EnglishGreeter{Message: m}
}

type EnglishGreeter struct {
	Message Message
}

func (g EnglishGreeter) Greet() Message {
	return g.Message
}

func (g EnglishGreeter) Grumpy() bool {
	var grumpy bool
	if time.Now().Unix()%5 == 0 {
		grumpy = true
	}
	return grumpy
}

type Event interface {
	Start()
}

func NewGreeterEvent(g Greeter) (GreeterEvent, error) {
	if g.Grumpy() {
		return GreeterEvent{}, errors.New("could not create event: event greeter is grumpy")
	}
	return GreeterEvent{Greeter: g}, nil
}

type GreeterEvent struct {
	Greeter Greeter
}

func (e GreeterEvent) Start() {
	msg := e.Greeter.Greet()
	fmt.Println(msg)
}

DIライブラリを使わずにEventを取得する

依存関係も少ないのであまり複雑な感じはしないですが、eventインスタンスを初期化するまでにいくつかのコンストラクタを呼んで準備していることがわかります。こうした定義がアプリケーションの大規模化とともに大きくなっていくと煩わしいということですね。

func main() {
	var event Event
	message := NewMessage("Hi there")
	greeter := NewEnglishGreeter(message)
	event, err := NewGreeterEvent(greeter)
	if err != nil {
		fmt.Printf("failed to create event: %s\n", err)
		os.Exit(2)
	}
	event.Start()
}

uber-go/dig

uber-go/digでのインスタンスを取得するまでの手続きは以下の通りです。

  1. dig.Newメソッドでコンテナを作成する。
  2. dig.Provideで取得したいインスタンスが依存する型のコンストラクタを登録する。
    オプションで実装しているインタフェースを設定する。
  3. c.Invokeで取得したいインスタンスを扱う関数を実行する。
func main() {
    // 1. コンテナの作成
	c := dig.New()
	err := c.Provide(func() string {
		return "Hi there"
	})
	if err != nil {
		fmt.Printf("failed to provide phrase: %s\n", err)
		os.Exit(2)
	}
    // 2. コンストラクタの登録
	err = c.Provide(NewMessage)
	if err != nil {
		fmt.Printf("failed to provide message: %s\n", err)
		os.Exit(2)
	}
    // 2. オプションで実装しているインタフェースを設定
	err = c.Provide(NewEnglishGreeter, dig.As(new(Greeter))) 
	if err != nil {
		fmt.Printf("failed to provide Greeter: %s\n", err)
		os.Exit(2)
	}
	err = c.Provide(NewGreeterEvent, dig.As(new(Event)))
	if err != nil {
		fmt.Printf("failed to provide GreeterEvent: %s\n", err)
		os.Exit(2)
	}
    // 3. インスタンスを取得して関数の実行
	err = c.Invoke(func(event Event) error {
		event.Start()
		return nil
	})
	if err != nil {
		fmt.Printf("failed to invoke GreeterEvent: %s\n", err)
		os.Exit(2)
	}
}

コード上のボリュームは大きくなっていますね。ただし、十分なコンストラクタを登録していれば、型毎の関係は意識せずともインスタンスが取得できています。

作成されたインスタンスはSingletonのスコープで管理される為、Greeterが不機嫌となったなら、当該コンテナはGreeterEventのコンストラクタ処理に必ず失敗します。これは使い方が悪いですね。

可変値を与えて都度インスタンスを初期化する思想のライブラリでもない為、Messageに登録するstringについてもdig.Provideで登録しています。

「コンストラクタ処理のエラー」、「登録しているコンストラクタが不足している場合のエラー」、「インスタンスを取得して実行した関数が返却するエラー」のすべてをc.Invokeのエラーとしてハンドリングすることになっています。

google/wire

google/wireでのインスタンスを取得するまでの手続きは以下の通りです。

  1. wire.goに取得したいインスタンスを戻り値に持つ関数を定義する。
    この時、wire.Buildに登録するコンストラクタ、または、定義した関数の引数で、取得したい型の依存解決に必要な型を網羅する。
  2. wireコマンドを実行することで、wire_gen.goファイルが作成され、インスタンスを取得する為の関数が生成されます。
  3. 生成された関数を呼び出してインスタンスを取得します。

wire_gen.goの関数を利用する為、wire.goには「// +build wireinject」のビルドタグを定義し、通常ビルドには含まれないようにしておきます。

  • wire.go
// 1. コンストラクタおよびインタフェースと実装型の関係を登録、引数の定義
func InitializeEvent(phrase string) (Event, error) {
	wire.Build(wire.Bind(new(Event), new(GreeterEvent)), NewGreeterEvent, wire.Bind(new(Greeter), new(EnglishGreeter)), NewEnglishGreeter, NewMessage)
	return GreeterEvent{}, nil
}
  • wire_gen.go
// 2. wireコマンドによって生成される関数
func InitializeEvent(phrase string) (Event, error) {
	message := NewMessage(phrase)
	greeter := NewGreeter(message)
	greeterEvent, err := NewGreeterEvent(greeter)
	if err != nil {
		return nil, err
	}
	return greeterEvent, nil
}
  • main.go
func main() {
    // 3. 関数を呼び出してインスタンスを取得
	event, err := InitializeEvent("Hi there")
	if err != nil {
		fmt.Printf("failed to create event: %s\n", err)
		os.Exit(2)
	}
	event.Start()
}

ファイルが多くなっていますが、wire.goはビルドには含めません。登録したコンストラクタが十分であるかはwireコマンドの実行時に評価され、必要十分なimportのみが定義される為、アプリケーションの実行時には課題となりません。

生成された関数InitializeEventを実行する度にEvent型インスタンスが取得される為、Greeterが不機嫌かどうかも実行の度に変わります。取得したインスタンスのスコープは呼び出し側で管理する必要があります。また、関数の引数として提供する型は、実行時に与えることができます。

DIライブラリを使うことで課題が解決できるか

再掲ですが、課題感は以下の3点でした。

  1. コンストラクタをたくさん呼び出して、やっとこさ初期化できるインスタンス
  2. 初期化の定義が大集合しがち。複数人が同じファイルをあれやこれやと編集することになる。
  3. 構造体が実装しているインタフェースを明示的に記載しないので、後から読むと困る場合が多い。

DIライブラリを用いることで、コンストラクタを登録しておけば、依存性を注入してインスタンスを返却してくれるため、複雑な依存関係の影響を受けにくくなると感じました。
また、今回は同一ファイルや同一パッケージでコンストラクタの登録を実施していますが、それぞれリソースに紐づくパッケージでコンストラクタの登録を行うようにすれば、大規模な初期化ファイルを複数人が触る影響も局所化できると想定されます。
いずれもインタフェースを実装する構造体を指定する制約がかえって、依存セットの構成を読み取りやすくしているように感じました。
総じてDIライブラリの恩恵を得ることはでき、課題の解消ないし影響を軽減できるかなといった印象です。

所感

uber-go/dig

Singletonのインスタンスを管理するDIコンテナなので、インスタンスの生成時に引数を個別に指定できないことは特に課題にならないと推測されます。DIの仕組みとしてJava、Spring Frameworkに近いですね。

google/wireと比較して柔軟に依存関係を解決できる一方で、暗黙的に依存解決をしてインスタンスを返されるところや、「DIコンテナのエラー」「コンストラクタのエラー」「処理結果としてのエラー」が同じ場所で取得される点はGoらしくない挙動に見え、違和感がありました。

google/wire

wire.goにコンストラクタを登録して、ビルドには含めない実装であるため、設定ファイルに依存セットを外部化しているような感覚があり好ましい方式だと感じました。

機能の比較で触れている通り、同じ型を同一の依存セットに登録できないのは課題となるケースが多いように感じるが、思想的に対応の予定はない様子。依存セットを分割したり、型を分けて避けるような対応が必要。

アプローチとして手続き型言語であるGoらしさのある仕組みだと感じました。gRPCのprotoといい、こういった仕組みがGoogleのブームなのだろうか。

終わりに

しっくり来たのはgoogle/wireでした。インスタンス初期化に際しての手順が生成されるのは、手間を減らすとともにGoの文化に則って可読性の高さを提供しています。ただし、同一の型を同時に扱う場合に課題があるため、ラップ型や引数をまとめた構造体、依存関係の分割が必要になる。そうした対応で構成を複雑にさせる懸念もあり、プロジェクトで受け入れられるかは検討が必要ですね。

最後まで読んでいただきありがとうございました。

参考文献

https://github.com/avelino/awesome-go
https://github.com/uber-go/dig
https://github.com/google/wire
https://go.dev/blog/wire
https://github.com/kazukousen/gouml
https://qiita.com/cpp0302/items/3c5254b840df6af24c10

26
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?