【Go】Gin + Fx on AWS Lambda
Goの勉強を兼ねて個人でWebアプリの開発を進めているのですが、
表題の構成が今後テンプレートとして使いまわせそうだったので
Publicリポジトリ1として公開しました。今回はその覚書も兼ねてです。
DIを取り入れたモチベーション
- (主にLambdaに依存する)実装を隠蔽したかった
- テスト容易にしたかった(テストしてるとは言っていない)
- なんとなくカッコいいから
fxとは?
Uberが公開しているDI用のOSSです。2
GoogleのWireが有名なようですが、
fxの方がリリースが活発だったので今回はfxを採用しました。
ソースコード
エントリポイント
Cold Start...と標準出力している点を除けば
一見Lambdaのソースコードとはわからないと思います。
package main
import (
"fmt"
"github.com/miyamo2theppl/go-gin-fx-on-lambda-template/api/hello/configure/fxapp"
"go.uber.org/fx"
)
var app *fx.App
func init() {
app = fxapp.FxApp
}
func main() {
fmt.Println("Cold Start...")
app.Run()
}
fxapp
mainで使用しているfx.Appはここで初期化しています。
依存関係はここに閉じ込めてあるので
LambdaからEC2等へ移管する際もソースの修正は①②を変更して
バニラgin用の実装に差し替えるだけで完了します。
package fxapp
import (
"github.com/miyamo2theppl/go-gin-fx-on-lambda-template/api/hello/application"
"github.com/miyamo2theppl/go-gin-fx-on-lambda-template/api/hello/presentation"
"github.com/miyamo2theppl/go-gin-fx-on-lambda-template/fxapp/runner"
"github.com/miyamo2theppl/go-gin-fx-on-lambda-template/lambda" // ①
"go.uber.org/fx"
)
var FxApp *fx.App
func init() {
FxApp = fx.New(
fx.Options(
application.Module,
presentation.Module,
lambda.Module, // ②
),
fx.Invoke(
func(lifecycle fx.Lifecycle, runner runner.FxAppRunner) {
runner.Run(lifecycle)
},
),
)
}
fx_provider
各実装と同一パッケージ内にfx.Option型の変数 Moduleを定義しています。
このModuleをfx.App生成時に読み込ませることで依存関係を解決してくれます。
package runner
import "go.uber.org/fx"
var Module = fx.Options(fx.Provide(NewLambdaRunner))
FxAppRunner
FxAppRunner interfaceではfx.appの開始/終了にフックする処理を定義しています。
LambdaRunnerではLambdaの起動、GinRunnerではginの起動をそれぞれOnStartイベントにフックする実装になっています。
繰り返しになりますが、実行環境がLambdaでなくなった場合は、
注入する実装をLambdaRunner→GinRunnerに差し替えるだけです。
package runner
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/lambda"
"github.com/miyamo2theppl/go-gin-fx-on-lambda-template/fxapp/runner"
"github.com/miyamo2theppl/go-gin-fx-on-lambda-template/lambda/handler"
"go.uber.org/fx"
)
type LambdaRunner struct {
handler *handler.Handler
}
func (r *LambdaRunner) Run(lifecycle fx.Lifecycle) {
lifecycle.Append(
fx.Hook{
OnStart: func(context.Context) error {
fmt.Println("Starting Lambda")
go lambda.Start(r.handler.Execute)
return nil
},
OnStop: func(context.Context) error {
fmt.Println("Stopping Lambda")
return nil
},
},
)
}
func NewLambdaRunner(handler *handler.Handler) runner.FxAppRunner {
return &LambdaRunner{handler}
}
package gin
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/miyamo2theppl/go-gin-fx-on-lambda-template/fxapp/runner"
"go.uber.org/fx"
)
type GinRunner struct {
engine *gin.Engine
uri []string
}
func (r *GinRunner) Run(lifecycle fx.Lifecycle) {
lifecycle.Append(
fx.Hook{
OnStart: func(context.Context) error {
fmt.Println("Starting Gin")
if len(r.uri) == 1 {
go r.engine.Run(r.uri[0])
} else {
go r.engine.Run()
}
return nil
},
OnStop: func(context.Context) error {
fmt.Println("Stopping Gin")
return nil
},
},
)
}
func NewGinRunner(engine *gin.Engine, uri ...string) runner.FxAppRunner {
uri_len := len(uri)
if uri_len > 1 {
panic(fmt.Sprintf("Too many arguments for NewGinRunner: %d", uri_len))
}
return &GinRunner{engine, uri}
}
もう一度main.goに戻るとこんなイメージの挙動です。
func main() {
fmt.Println("Cold Start...")
app.Run() // ← ここでOnStartが実行される
}
lambda-gin
Lambda上でginを実行するための実装についてはほぼサンプル3通りですが
fxの恩恵を受けるためにHandlerはラッパーとなる構造体を用意しています。
package handler
import (
"context"
"github.com/aws/aws-lambda-go/events"
ginadapter "github.com/awslabs/aws-lambda-go-api-proxy/gin"
)
type Handler struct {
ginLambda *ginadapter.GinLambda
}
func (r Handler) Execute(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
return r.ginLambda.ProxyWithContext(ctx, req)
}
func NewHandler(ginLambda *ginadapter.GinLambda) *Handler {
return &Handler{ginLambda}
}
参考
Dependency Injection with Go-Fx