Go では依存性注入(DI)のためのコードを自動生成するツールである wire が良く利用されるようでので、早速導入したいと思います。

チュートリアルを自分が今作っているマイクロブログに当てはめただけです。

DI導入前

導入前は以下のように、自前でインスタンスを生成して引数として渡していました。

func InitializeServer() *controllers.Server {
    commentRepository := repositories.NewCommentRepositoryImpl()
    commentUsecase := usecases.NewCommentUsecase(commentRepository)
    server := controllers.NewServer(commentUsecase)
    return server
}

これはこれでシンプルなのですが、repositoryやusecaseが増えてくると大変そうです。
wireは上記のようなソースコードを自動生成してくれるツールです。

wireのインストール

go install github.com/google/wire/cmd/wire@latest

wire.goを記述

wireは、wire.goとそこで指定されているコンストラクタ関数から自動でDI用のコードを自動生成してくれます。
今回のケースでは、以下のようにwire.goを記述しました。
wire.Build()で各構造体のコンストラクタ関数を列挙しておくと、勝手にソースコードから関連性を把握してくれるようです。

//go:build wireinject
// +build wireinject

package main

import (
    "github.com/google/wire"
    "github.com/rinoguchi/microblog/adapters/controllers"
    "github.com/rinoguchi/microblog/adapters/repositories"
    "github.com/rinoguchi/microblog/usecases"
)

func InitializeServer() *controllers.Server {
    wire.Build(
        controllers.NewServer,
        usecases.NewCommentUsecase,
        repositories.NewCommentRepositoryImpl,
    )
    return &controllers.Server{}
}

DI用のコードを自動生成

以下のコマンドでDI用のコード(wire_gen.go)を自動生成します。

wire
> wire: github.com/rinoguchi/microblog: wrote /Users/xxx/xxx/microblog/wire_gen.go

トラブルシューティング

wireの設定や実装がおかしいとwire実行時にエラーが発生します。

  • wire.Buildでコンストラクタ関数の指定が足らない場合
    wire
    > wire: /Users/xxx/xxx/microblog/wire.go:13:1: inject InitializeServer: no provider found for github.com/rinoguchi/microblog/entities.CommentRepository
    >  needed by *github.com/rinoguchi/microblog/usecases.CommentUsecase in provider "NewCommentUsecase" (/Users/ryoichiinoguchi/workplace/microblog/usecases/comment_usecase.go:13:6)
    >  needed by *github.com/rinoguchi/microblog/adapters/controllers.Server in provider "NewServer" (/Users/xxx/xxx/microblog/adapters/controllers/server.go:14:6)
    > wire: github.com/rinoguchi/microblog: generate failed
    > wire: at least one generate failure

wire_gen.goの内容

DI導入前に自前で実装していたコードと全く同じ内容が自動再生されていました。

// Code generated by Wire. DO NOT EDIT.

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

package main

import (
    "github.com/rinoguchi/microblog/adapters/controllers"
    "github.com/rinoguchi/microblog/adapters/repositories"
    "github.com/rinoguchi/microblog/usecases"
)

// Injectors from wire.go:

func InitializeServer() *controllers.Server {
    commentRepository := repositories.NewCommentRepositoryImpl()
    commentUsecase := usecases.NewCommentUsecase(commentRepository)
    server := controllers.NewServer(commentUsecase)
    return server
}

main.goから呼び出し

最後に、mian.goから対象のコードを呼び出せば完了です。

package main

import (
    "fmt"
    "net/http"
    "os"

    middleware "github.com/deepmap/oapi-codegen/pkg/chi-middleware"
    "github.com/go-chi/chi/v5"
    "github.com/rinoguchi/microblog/adapters/controllers"
)

func main() {
    swagger, err := controllers.GetSwagger()
    if err != nil {
        fmt.Fprintf(os.Stderr, "Error loading swagger spec\n: %s", err)
        os.Exit(1)
    }
    swagger.Servers = nil
    server := InitializeServer() // <==== この部分
    router := chi.NewRouter()
    router.Use(middleware.OapiRequestValidator(swagger))
    controllers.HandlerFromMux(server, router)
    http.ListenAndServe(":8080", router)
}

アプリケーションの実行

以下のようにしてアプリケーションを実行します。

go run .

main.goを指定すると、以下のようにwire.goの内容が読み込まれずにエラーが発生します。

go run main.go
# command-line-arguments
./main.go:20:12: undefined: InitializeServer

ビルドしてから実行するのもありです。

# ビルド(microblogという実行ファイルが作成される)
go build
# 実行
microblog

さいごに

今回、実装したソースコードは以下のリポジトリで公開してます。
https://github.com/rinoguchi/microblog

Posted in: go