🍣

2021年も終わるので、0からREST APIを作るならどうしようかなを考えてみた(Go編)※WIP

2021/11/07に公開

GithubのURLだけくれたら適当にみとくよ

https://github.com/shinofara/modern-go-application-for-me-2021

前提

MySQLを使ったアプリケーション開発がまだまだ主流と感じてますので、MySQLを使う事と、REST APIで有ること。そしてAPIの仕様はOpen APIをベースに、スキーマ駆動で開発していく事を大前提としています。

今回お話する領域について

devでは、下記のポイントを重点的にかければと思ってます。
※他にも考えないと行けないなと思う事とか、見直さないとなと思ってることもありますが、とりあえずできたところベースで

  1. Open API V3
    3. https://github.com/deepmap/oapi-codegen
  2. Validation
    5. https://github.com/go-ozzo/ozzo-validation
  3. ORM(Object Relational Mapper)
    7. https://entgo.io/ent
  4. Migration
    9. https://entgo.io/ent
  5. Logging
    11. https://github.com/rs/zerolog
  6. APM(Application Performance Monitoring)
    13. https://go.opentelemetry.io/otel
  7. DI(Dependency Injection)
    15. https://go.uber.org/dig

Open API V3

Open APIを採用する理由の一つとして仕様を外部に公開する事が考えられます。外部と言わないまでも別のアプリケーションと連携する際に、スキーマを連携すればクライアントコードを比較的簡単に生成できるメリットもありますね。

Open APIのスキーマと開発したREST APIとで仕様を同期する方法としては、以下の2点考えられます。

  1. コードからジェネレート
  2. Open APIのスキーマを変更して、サーバ側のコードをジェネレート

今回ここでは、2番目を採用しました。
特に何か特別な事をしてることはなく、下記のツールを利用しています。
https://github.com/deepmap/oapi-codegen

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://zenn.dev/mattn/articles/893f28eff96129
こちらに書かれている物を参考にしました。
https://github.com/go-ozzo/ozzo-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を使うことで、下記の差分の通りある程度しようとして分離しつつも柔軟性をもって書けるため、今の所良いのではと感じています。

https://github.com/shinofara/modern-go-application-for-me-2021/compare/6ec7ad72d68f1ff7d3ed34940130c2c7a5e28083..2be6189452c5c8df8f6604885c3193a777f678a9

ORM(Object Relational Mapper)

https://entgo.io/ent
https://github.com/shinofara/modern-go-application-for-me-2021/blob/master/repository/task.go

Migration

https://entgo.io/ent

https://github.com/shinofara/modern-go-application-for-me-2021/blob/master/cmd/migration/main.go

Logging

https://github.com/rs/zerolog
https://github.com/shinofara/modern-go-application-for-me-2021/blob/2be6189452c5c8df8f6604885c3193a777f678a9/infrastructure/logger/logger.go#L44

APM(Application Performance Monitoring)

https://go.opentelemetry.io/otel
https://github.com/shinofara/modern-go-application-for-me-2021/blob/master/cmd/api/main.go#L98

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)
}

https://github.com/shinofara/modern-go-application-for-me-2021/blob/master/cmd/api/main.go#L72

上の例では雑に一部抜粋しましたが、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にわたすことで上記の問題を解決しています。

https://github.com/shinofara/modern-go-application-for-me-2021/blob/master/cmd/api/main.go#L68

レイヤー設計に関して

WIP

Discussion