2021年も終わるので、0からREST APIを作るならどうしようかなを考えてみた(Go編)※WIP
GithubのURLだけくれたら適当にみとくよ
前提
MySQLを使ったアプリケーション開発がまだまだ主流と感じてますので、MySQLを使う事と、REST APIで有ること。そしてAPIの仕様はOpen APIをベースに、スキーマ駆動で開発していく事を大前提としています。
今回お話する領域について
devでは、下記のポイントを重点的にかければと思ってます。
※他にも考えないと行けないなと思う事とか、見直さないとなと思ってることもありますが、とりあえずできたところベースで
- Open API V3
3. https://github.com/deepmap/oapi-codegen - Validation
5. https://github.com/go-ozzo/ozzo-validation - ORM(Object Relational Mapper)
7. https://entgo.io/ent - Migration
9. https://entgo.io/ent - Logging
11. https://github.com/rs/zerolog - APM(Application Performance Monitoring)
13. https://go.opentelemetry.io/otel - DI(Dependency Injection)
15. https://go.uber.org/dig
Open API V3
Open APIを採用する理由の一つとして仕様を外部に公開する事が考えられます。外部と言わないまでも別のアプリケーションと連携する際に、スキーマを連携すればクライアントコードを比較的簡単に生成できるメリットもありますね。
Open APIのスキーマと開発したREST APIとで仕様を同期する方法としては、以下の2点考えられます。
- コードからジェネレート
- Open APIのスキーマを変更して、サーバ側のコードをジェネレート
今回ここでは、2番目を採用しました。
特に何か特別な事をしてることはなく、下記のツールを利用しています。
go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest -generate types -package openapi openapi/src/generated/openapi/openapi.yaml > openapi/types.gen.go;
go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest -generate chi-server -package openapi openapi/src/generated/openapi/openapi.yaml > openapi/server.gen.go
go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest -generate spec -package openapi openapi/src/generated/openapi/openapi.yaml > openapi/spec.gen.go
Validation
こちらに書かれている物を参考にしました。
これまでは、フルスクラッチで実装したり、https://github.com/go-playground/validator を採用して、Exampleにある通り
type User struct {
FirstName string `validate:"required"`
LastName string `validate:"required"`
Age uint8 `validate:"gte=0,lte=130"`
Email string `validate:"required,email"`
FavouriteColor string `validate:"iscolor"` // alias for 'hexcolor|rgb|rgba|hsl|hsla'
Addresses []*Address `validate:"required,dive,required"` // a person can have a home and cottage...
}
と、structのtagにガッツリ書いて対応するなどしてました。しかしアプリケーションの仕様は変わりゆくので、これだけでは対応できない要件などが出てくると、tagでのvalidationに加えて、自前でvalidationロジックの追加などが必要でした。※もしくは拡張
そうなってくると、validationに関しての責任も曖昧になってくるため、自前で最初から書いてるほうがいいのではと思うこともしばしば(僕の使い方がNGなケースもあります)
しかし、go-ozzo/ozzo-validationを使うことで、下記の差分の通りある程度しようとして分離しつつも柔軟性をもって書けるため、今の所良いのではと感じています。
ORM(Object Relational Mapper)
Migration
Logging
APM(Application Performance Monitoring)
DI(Dependency Injection)
Clean Architectureとか、DDD という考え方とか採用していると更にですが、レイヤー間の依存解決をどこでするか、どうやるかとか最初は頭を悩ませますよね。なやませました。そもそもそんな事関係なく、様々なパッケージを採用しているだけでも考える事が多いです。
例えばDBとか、Loggingとかの初期設定はmain実行〜HTTP起動までの間に解決して、HTTP起動後のhandler処理では使うだけにしたい。それだけではなくInfrastructure, Repository, UseCaseなどどうやってHandler実行時に呼び出すかも頭を悩ませます。毎回書いてもいいけど、毎回隔離必要性は特にないので、これも可能ならHTTP起動前に解決しておきたい。
※あとからなんとでもなるので最初の最初は気にせず書くのも一つの手段。悩みすぎて進まないのも避けたいです
今回依存解決には、Uberが公開してるgo.uber.org/digを採用しました。
詳細はコードを見てもらうほうが早いですが
provides := []interface{}{
context.Background,
mailer.NewDummyMailer,
handler.NewHandler,
...以下略
}
container := dig.New()
// Providerに登録
for _, p := range provides {
if err := container.Provide(p); err != nil {
panic(err)
}
}
// Invokeで実行させたいfunctionを渡すことで、functionの実行に必要な依存がすべて解決されます。
if err := container.Invoke(Server); err != nil {
panic(err)
}
上の例では雑に一部抜粋しましたが、ProvideやInvokeに渡したfunctionの実行に必要な引数を、それぞれの戻り値からよしなに解決して、よしなにしてくれます。function以外渡せないので、configのような構造体を渡したい場合は、少し手間が必要です。
type Config struct {
dig.Out
DB *database.Config
Trace *trace.Config
Logger *logger.Config
}
func (cfg Config) Clone() Config {
return cfg
}
cfg.Clone
というfunctionを作成して、こちらをprovideにわたすことで上記の問題を解決しています。
レイヤー設計に関して
WIP
Discussion