いろいろなビルド用の分岐にタグを使ってみる(例えばlambda用のビルド)
いい感じにできないか考えたりしていた。アイデア段階で途中だけど途中経過をメモ。
TODOアプリを模したコードがあったとして、以下の様な操作が提供されているとする。
type TodoApp interface { List() ([]Todo, error) Add(todo Todo) (Todo, error) }
これをいろいろな形態で提供してみたい。 例えば、1つは手元で動かすwebアプリ、もう1つはaws lambda。
このときにいい感じに取り扱うことができないか?と言う話。まぁ今回は実際のデプロイまでは行わず、ビルドしてバイナリを作るところまででおしまいにする。
$ ls a* a.out a.out.lambda.add a.out.lambda.list
ファイル構成
以下の様なファイル構成で実験してみる。hello world的なコードなのであまり真面目に考えない。 interactor.goが冒頭であげたインターフェイスを実装しているようなコード。
$ tree . . ├── Makefile ├── interactor.go ├── main.go ├── main_lambda_add.go └── main_lambda_list.go 0 directories, 5 files
それぞれのファイルは以下の様な形になっている。
main.go
// +build !lambda package main import ( "encoding/json" "log" "net/http" "os" "github.com/go-chi/chi" "github.com/go-chi/render" ) type Controller struct { Interactor *Interactor } ... // 省略 (全コードは付録として) func main() { r := chi.NewRouter() c := &Controller{Interactor: GetInteractor()} r.Get("/", c.List) r.Post("/", c.Add) addr := os.Getenv("ADDR") if addr == "" { addr = ":44444" } if err := http.ListenAndServe(addr, r); err != nil { log.Fatalf("!!%+v", err) } }
main_lambda_list.go
// +build lambda // +build list package main import ( "github.com/aws/aws-lambda-go/lambda" ) func main() { ir := GetInteractor() lambda.Start(ir.List) }
main_lambda_add.go
// +build lambda // +build add package main import ( "github.com/aws/aws-lambda-go/lambda" ) func main() { ir := GetInteractor() lambda.Start(ir.Add) }
ビルド
という形にファイルを分けた上で、タグによって利用するmain.goを分岐させてビルドしてみる。
$ make clean default rm -f a.out* go build -o a.out go build -o a.out.lambda.list --tags lambda,list go build -o a.out.lambda.add --tags lambda,add
a.outはmain.go。それ以外はlambda用のバイナリ。このmain.go達がかなりコピペ感があって微妙だなーと言う気持ちになったりしていた。
このときのMakefile。
TARGETS := $(shell cat Makefile | grep '^a\.out' | cut -d : -f 1) default: $(TARGETS) a.out: go build -o $@ a.out.lambda.list: go build -o $@ --tags lambda,list a.out.lambda.add: go build -o $@ --tags lambda,add clean: rm -f a.out* .PHONY: clean
付録: lambdaやhandler/controllerの実装の詳細
main.goの詳細
長すぎて省略したmain.goの全貌はこんな感じ。
main.go
// +build !lambda package main import ( "encoding/json" "log" "net/http" "os" "github.com/go-chi/chi" "github.com/go-chi/render" ) type Controller struct { Interactor *Interactor } func (c *Controller) List(w http.ResponseWriter, r *http.Request) { items, err := c.Interactor.List() if err != nil { render.Status(r, 500) render.JSON(w, r, map[string]interface{}{"message": err.Error()}) return } render.JSON(w, r, map[string]interface{}{"items": items}) } func (c *Controller) Add(w http.ResponseWriter, r *http.Request) { var item Todo decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&item); err != nil { render.Status(r, 400) render.JSON(w, r, map[string]interface{}{"message": err.Error()}) } added, err := c.Interactor.Add(item) if err != nil { render.Status(r, 500) render.JSON(w, r, map[string]interface{}{"message": err.Error()}) return } render.JSON(w, r, added) } func main() { r := chi.NewRouter() // このままが良いのか、interactorの粒度を細かくしたほうが良いのかはわからない // github.com/awslabs/aws-lambda-go-api-proxy を使うよりは意義があるのではないか? // Controllerが肥大化するならたぶん分けたほうが良い。Interactorも同様ならhandler関数を生成する関数を定義する形のほうが綺麗かもしれない c := &Controller{Interactor: GetInteractor()} r.Get("/", c.List) r.Post("/", c.Add) addr := os.Getenv("ADDR") if addr == "" { addr = ":44444" } if err := http.ListenAndServe(addr, r); err != nil { log.Fatalf("!!%+v", err) } }
Handlerを共有?Interactorを共有?
この実装とは別の考え方として、Handlerを共有する方法もある。awslabs/aws-lambda-go-api-proxy
を使う。
今回はController(Handler)がInteractorを持ち、これを共有するということを考えてみた1。
ちなみに、lambdaは以下のようなインターフェイスを許容する(lambda.Start()
に渡せるのは以下の様なシグネチャを持つ関数)。
func () func () error func (TIn) error func () (TOut, error) func (TIn) (TOut, error) func (context.Context) error func (context.Context, TIn) error func (context.Context) (TOut, error) func (context.Context, TIn) (TOut, error)
詳しくはドキュメントに
ちなみにミドルウェアのようなことがしたければlambda/handlertrace
ある。
使い方はPRを見るとわかりやすい。
おわりに
main.goをいい感じに書かずに済んだりしないかな。。あと、api-gatewayまで含めてlambdaにdeployしちゃいたい。。
gist
-
主題はビルドの話なので設計の話は枝葉の話ではある↩