はじめに
普段Java、Spring Frameworkを使って開発していると、Goで開発する場合の依存解決には苦労することがあるように思います。
Dependency Injection(以下DI)をやるとして、階層の深い依存関係のある構造体を初期化しようとすると、あれもこれもとコンストラクタを呼ぶことになります。また、往々にしてそういった構造体は1つではないので、どでかい初期化処理が生まれることになります。
そこで、本記事では、GoにいくつかあるDependency Injectionをサポートするライブラリについて調べていきます。
Goの依存解決の課題は何か
作成するアプリケーションや、開発体制が大きくなればなるほど、以下のような課題が出てきます。
- コンストラクタをたくさん呼び出して、やっとこさ初期化できるインスタンス
- 初期化の定義が大集合しがち。複数人が同じファイルをあれやこれやと編集することになる。
- 構造体が実装しているインタフェースを明示的に記載しないので、後から読むと困る場合が多い。
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/dig
、google/wire
についてみていくこととします。(uber-go/fx
はuber-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
が実装しています。 -
GreeterEvent
はGreeter
インタフェースに依存しています。 -
Greeter
インタフェースをEnglishGreeter
が実装しています。 -
Greeter
は作成されたタイミングでたまに不機嫌(Grumpy
)です。不機嫌である場合、GreeterEvent
のコンストラクタ処理を失敗させます。 -
Message
はString
型をラップしています。
※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
でのインスタンスを取得するまでの手続きは以下の通りです。
-
dig.New
メソッドでコンテナを作成する。 -
dig.Provide
で取得したいインスタンスが依存する型のコンストラクタを登録する。
オプションで実装しているインタフェースを設定する。 -
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
でのインスタンスを取得するまでの手続きは以下の通りです。
-
wire.go
に取得したいインスタンスを戻り値に持つ関数を定義する。
この時、wire.Build
に登録するコンストラクタ、または、定義した関数の引数で、取得したい型の依存解決に必要な型を網羅する。 -
wire
コマンドを実行することで、wire_gen.go
ファイルが作成され、インスタンスを取得する為の関数が生成されます。 - 生成された関数を呼び出してインスタンスを取得します。
※ 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点でした。
- コンストラクタをたくさん呼び出して、やっとこさ初期化できるインスタンス
- 初期化の定義が大集合しがち。複数人が同じファイルをあれやこれやと編集することになる。
- 構造体が実装しているインタフェースを明示的に記載しないので、後から読むと困る場合が多い。
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