Software Engineer Blog

エンジニアブログです。技術情報(Go/TypeScript/k8s)や趣味の話も書くかもです。

Go-Cloudで利用されているDIツールWireについて

概要

Go-Cloud Projectで利用されている、 Wire と呼ばれる
Dependency Injection Tool について触ってみました。
(README.md を試した形です)

Install

github.com

wire は go get でインストールします。

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

実装

github.com

基本的な使い方

ProviderとInjecterという2つの概念を持ちます

Provider

Providerは、依存させたいstructの実体を返却します。

例)
依存させたいstructの実態を定義します。

// foo.go
type Foo struct{
    Name string
}

Provider関数を定義します。

func ProviderFoo(name string) Foo {
    return Foo{
        Name: name,
    }
}

Providerの利用元も実装します。
(今回は1つですが) ProviderSetを定義することで、複数のProviderをまとめて処理します。
main.go

// ProviderSet
var SuperSet = wire.NewSet(ProviderFoo)

func main() {
    // flagで名前を切り替えられるようにしてみます
    n := flag.String("n", "foo", "foo name")
    flag.Parse()

    foo, err := setUp(context.Background(), *n)
    if err != nil {
        log.Fatalln(err)
    }

    // fooの名前を出力
    fmt.Println(foo.Name)
}

Injector

Injectorは、Provider関数を利用して実体を実装へinjectします。
injectorのコードは仮実装を元に、wire が生成してくれます。

例)
Injectorの仮実装を定義します。
コード生成の元コードのため、buildタグを付与して build時に利用されないようにします。 injector.go

// buildタグを付与
//+build wireinject

// 実装
func setUp(ctx context.Context, name string) (Foo, error) {
    // ProviderをBuildする
    wire.Build(SuperSet)
    // 特に意味はない
    return Foo{}, nil
}

ここで、injectorの実装を生成していきます。 下記コマンドを実行します。
(go getで入れているので、バイナリが$GOPATH/bin配下に配置されてます。)

% wire

wire コマンドを実行すると、下記のような wire_gen.go が生成されます。
参考) wire_gen.go

// Code generated by Wire. DO NOT EDIT.

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

package main

import (
        context "context"
)

// Injectors from inject.go:

func setUp(ctx context.Context, n string) (Foo, error) {
        foo := ProviderFoo(n)
        return foo, nil
}

実行

wire_gen.go が生成された段階で、buildして実行してみます。

% go build -o wire-sample

(補足)
buildタグで、!wireinject となっているため go build 時は、 injector.goの代わりにwire_gen.go が利用されます。

実行

% ./wire-sample
foo

# ちゃんとフラグで渡した値が利用されている
% ./wire-sample -n bar
bar

ちなみに、一度 wire_gen.go を生成したあとで、修正したい場合は、 go generate を利用します。

# 再ビルド
% go generate && go build -o wire-sample

Tips

① 独自型を定義してProviderへ渡す

Providerに、一般的な型(string, int, etc..)を利用すると、コードを生成する際に一意に定まらず、期待しない動作になる可能性があります。
そのため、独自型で定義してsetUpの引数に渡してあげることで、確実に狙ったProviderの引数に渡すことができます。

例)
injectしたいstruct

type Foo struct {
    Name: string
}

type Hoge struct {
    Hoge: string
}

type FooHoge struct {
    Foo Foo
    Hoge Hoge
}

providerの実装

// 型定義
type fooName string

func ProviderFoo(name fooName) Foo {
    return Foo{
        Name: string(name),
    }
}

func ProviderHoge(name string) Hoge {
    return Hoge{
        Name: name,
    }
}

func ProviderFooHoge(foo Foo, hoge Hoge) FooHoge {
    return FooHoge{
        Foo: foo,
        Hoge: hoge,
    }
}
var SuperSet = wire.NewSet(ProviderFoo, ProviderHoge, ProviderFooHoge)

func main() {
    fh, _ := setUp(ctx, "foo", "hoge")
    fmt.Println(fh.Foo.Name)
    fmt.Println(fh.Hoge.Name)
}

injector.go

func setUp(ctx context.Context, fn fooName , n string) (FooHoge, error) {
    wire.Build(SuperSet)
    return Foo{}, nil
}

↓ go generate

// generateした際に、stringのままだとFoo/Hogeの
// どちらの名前かわからなくなってしまう
func setUp(ctx context.Context, fn fooName , n string) (FooHoge, error) {
    foo := ProviderFoo(fn)
    hoge := ProviderHoge(fn)
    fooHoge := ProviderFooHoge(foo, hoge)
    reutnr fooHoge, nil
}

所感

GoでDIする際に、あまりツールを利用できていなかったので、 利用が増えてくれば、実コードに入れるのもありかと思いました。 DIに関しては、 Guice を一度触れていたので、比較的すんなり入ってきました。 テスタビリティがきちんとあがっているかは、テスト書いてみて確認したいです。

参考