Go
AWS
GoogleCloudPlatform

【Go言語】 Go Cloud で使われている Wire というツールでの依存性注入が便利かもしれない。

Go-Cloud Project が発表されて久しいですね。
Go Cloudは、クラウド (AWSやGCP) 間で使用頻度の高いサービスの汎用的なAPIを開発しているプロジェクトです。コードを変えずにAWSやGCPに対応できるサービスを開発できるのは、かなり便利ですね。まだβ版ですが、実際に安定すれば、是非とも組み込みたいパッケージです。

google/go-cloud

その中で利用されている、wire と呼ばれる Dependency Injection Tool に触ってみました。簡単に説明すると依存性注入したコードを自動で生成してくれるツールです。

wire の実力を確認しよう!

今回は go-cloud にある example を参考にしてます。
最終的にはこんな感じになってます。

.
├── main.go
├── wire.go
└── wire_gen.go

wire 無しの場合

まずは説明のために、wire なしの場合です。 main.go を作ります。

package main

import (
    "fmt"
)

type Message string

type Greeter struct {
    Message Message
}

type Event struct {
    Greeter Greeter
}

func NewMessage() Message {
    return Message("hello wire")
}

func NewGreeter(m Message) Greeter {
    return Greeter{Message: m}
}

func NewEvent(g Greeter) Event {
    return Event{Greeter: g}

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

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

func main() {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)
    event.Start()
}

hello wire と出してくれる簡単なコードです。dependency injection design principle に沿ってます。これにより、テスタアビリティが高くなり、依存関係を別の依存関係に簡単に置き換えることも容易になります。しかし、上のコードではmain関数の中で初期化のステップが多いです。上のような簡単なコードならマシですが、依存性が複雑化していくと依存性注入も大変になります。そこで wire が登場します。

wire を使ってみる

まずは wire コマンドが使えるように go get しておきましょう。

$ go get github.com/google/go-cloud/wire/cmd/wire

そして、main関数を下記に書き換えます

func main() {
    e := InitializeEvent()

    e.Start()
}

初期化していたコードが1行になりました。
そして 同階層に wire.go を書きます。

//+build wireinject

package main

// The build tag makes sure the stub is not built in the final build.
import "github.com/google/go-cloud/wire"

func InitializeEvent() Event {
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}
}

wire.Build() で欲しい依存性を注入して初期化しています。ここで大事なのは最初の行です。

//+build wireinject

これはビルド制約の書き方です。buildの際にこのファイルを無視します。この1行の下にもう1行空白行もお忘れなく。
ビルド制約については下記参照
https://godoc.org/go/build#hdr-Build_Constraints

このファイルから wire コマンドで dependency injection を自動生成します。

$ wire

そうすると wire_gen.go が生成されています。中をみて見ると、最初に作ったmain関数と同じ処理が行われるコードが生成されています。

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from wire.go:

func InitializeEvent() Event {
    message := NewMessage()
    greeter := NewGreeter(message)
    event := NewEvent(greeter)
    return event
}

これで wire を使って依存性注入が自動生成できました。

$ go build -o try-wire
$ ./try-wire
hello wire

サンプルだと、シンプルすぎて恩恵を感じにくいですが、go-cloud のような依存関係をガンガン入れていくようなツールや、アプリケーションでは便利かもしれません。
go-cloud の 下記のサンプルでは下のコードのように wire.Build されています。

https://github.com/google/go-cloud/blob/master/samples/guestbook/README.md)

// setupAWS is a Wire injector function that sets up the application using AWS.
func setupAWS(ctx context.Context, flags *cliFlags) (*application, func(), error) {
    // This will be filled in by Wire with providers from the provider sets in
    // wire.Build.
    wire.Build(
        awscloud.AWS,
        applicationSet,
        awsBucket,
        awsMOTDVar,
        awsSQLParams,
    )
    return nil, nil, nil
}

// some injector ...

テストや実行環境によって、依存を切り替えたい場合は、go-cloud の example のように inject_aws.go や inject_gcp.go など何パターンかファイルを作っておいて、
必要に応じて wire コマンドを叩くのが良さそうです。