eviry tech & service blog

「株式会社エビリー」の社員ブログです。弊社では、クラウド型動画配信サービス「millvi」、ソーシャル動画データ及び分析サービス「kamui tracker」、YouTube総合メディア「かむなび」を開発・提供しています。https://eviry.com/

wireを使ってgoでDIを実現する

これは旧eviry tech blogから移行した記事です。

tkです。
今回新しい試みとしてgoを使ったAPIサーバーを構築することになりました。
構成としてはcontroller/service/repositoryに分離し、テストも実装しながら進めていこうということになっています。

そうなると、モジュール間はできるだけ疎結合になってくれていると実装もしやすくテストも書きやすくなります。
なので、DI(依存性の注入: Dependency Injection)が実現できれば疎結合を保ったままモジュール間を連携できると考え、今回はgoogle/wireを使用してDIを実現してみることにしました。

公式のページはこちらです。
GitHub - google/wire: Compile-time Dependency Injection for Go

ここでは、「Sampleモデルの情報をrepositoryからservice経由で取得して結果を返すcontrollerのメソッド」を実装してみます。

package model

type Sample struct {
    Id int
    Name string
}
package repository

import "wire-sample/model"

type SampleRepository struct {}

func NewSampleRepository() SampleRepository {
    return SampleRepository{}
}

func (sr SampleRepository) GetSample() model.Sample {
    return model.Sample{Id: 1, Name: "sample"}
}
package service

import (
    "wire-sample/model"
    "wire-sample/repository"
)

type SampleService struct {
    sr repository.SampleRepository
}

func NewSampleService(sr repository.SampleRepository) SampleService {
    return SampleService{sr:sr}
}

func (ss SampleService) GetSample() model.Sample {
    return ss.sr.GetSample()
}
package controller

import (
    "wire-sample/model"
    "wire-sample/service"
)

type SampleController struct {
    ss service.SampleService
}

func NewSampleController(ss service.SampleService) SampleController {
    return SampleController{ss:ss}
}

func (sc SampleController) GetSample() model.Sample {
    return sc.ss.GetSample()
}

それぞれ生成したインスタンスを返すメソッドを作成しています。
このメソッドにserviceやrepositoryを渡すことで、依存するものを外部から渡せるようにしています。

例えば、これを使ってcontrollerを作ろうとすると、

sampleRepository := repository.NewSampleRepository()
sampleService := service.NewSampleService(sampleRepository)
sampleController := controller.NewSampleController(sampleService)

という実装になると思いますが、google/wireを使うとこれを自動生成することが可能で、独立したメソッドとして実装してくれます。

まずはgoogle/wireをインストールします。

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

次に、wire.goというファイルを作ります。
ここに、「どのインスタンスは何に依存しているか、それらを生成するためのメソッドは何か」を定義します。

//+build wireinject

package main

import (
    “github.com/google/wire”
    “wire-sample/controller”
    “wire-sample/repository”
    “wire-sample/service”
)

func InitializeSampleController() controller.SampleController {
    wire.Build(controller.NewSampleController, service.NewSampleService, repository.NewSampleRepository)
    return controller.SampleController{}
}

あとはコマンドラインからwireを実行します。
すると、wire_gen.goというファイルが作られ、その中にwire.goの定義を元にDIされたインスタンスを生成するためのメソッド(上記の場合、SampleControllerを返すInitializeSampleControllerメソッド)が実装されています。

// Code generated by Wire. DO NOT EDIT.

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

package main

import (
    “wire-sample/controller”
    “wire-sample/repository”
    “wire-sample/service”
)

// Injectors from wire.go:

func InitializeSampleController() controller.SampleController {
    sampleRepository := repository.NewSampleRepository()
    sampleService := service.NewSampleService(sampleRepository)
    sampleController := controller.NewSampleController(sampleService)
    return sampleController
}

これを使えばmainメソッドの中でDIされたcontrollerを利用できます。

func main() {
    sc := InitializeSampleController()
    http.HandleFunc("/sample", func(w http.ResponseWriter, r *http.Request) {
        sample := sc.GetSample()
        body, _ := json.Marshal(sample)
        fmt.Fprintf(w, string(body))
    })
    http.ListenAndServe(":8080", nil)
}

また、controllerやservice、repositoryは実装したものの中では直接インスタンスのやり取りをしていないので、疎結合が保たれてテストも書きやすくなっています。
これ以外にも、公式のページにあるように条件に合わせて生成物をコントロールすることもできるので、まずはそちらを確認してみてください。

以上になります。