GoSuda

GoおよびOpenAPIエコシステム

By iwanhae
views ...

序論

Go言語でProduction Backendサーバーを開発する際、ほとんどの開発者が最初に直面する難題の一つは、以下の通りです。

APIドキュメント、どうすればいいんだ…?

これについて少し調べてみると、OpenAPI仕様に合わせたドキュメントを作成することが有益であるという事実に気づき、自然にOpenAPIと連携するライブラリを探すことになります。しかし、このような決定を下しても、次の問題が存在します。

OpenAPI関連のライブラリがたくさんあるけど…どれを使えばいいんだ…?

この文書は、このような状況を経験しているGo入門者のために作成した簡単なライブラリ紹介文です。2024年末基準で作成された文書であり、言語生態系は常に流動的に変化するため、参考にしながら常に最新の動向も確認することをお勧めします。

OpenAPI に対するライブラリの戦略

既にご存知の部分だとは思いますが、OpenAPIはREST APIを明確に定義し、文書化するための仕様です。APIのエンドポイント、リクエスト、レスポンス形式などをYAMLまたはJSON形式で定義し、開発者だけでなくフロントエンド、バックエンドのコード生成を自動化することで、無意味な繰り返しを減らし、ささやかなヒューマンエラーを減らすのに大きく役立ちます。

このようなOpenAPIをプロジェクトと自然に結合させるため、Go生態系のライブラリは大きく次の三つの戦略を取ります。

1. GoのコメントをOpenAPI仕様文書として組み合わせる

OpenAPIに合わせてAPIを開発する際に難しい点の一つは、実際のドキュメントと当該ドキュメントを実装したコードが別のファイルとして全く異なる場所に存在するため、コードを更新したのにドキュメントを更新していなかったり、ドキュメントは更新したのにコードを更新できなかったりする状況が意外と頻繁に発生することです。

簡単な例を挙げると、

  1. ./internal/server/user.goというファイルの中でAPIに対するロジックを修正したが
  2. 実際のドキュメントは./openapi3.yamlに存在し、これに対する変更をうっかり忘れてしまうことがあります。
  3. このような変更事項に対する問題を認識できずにPull Requestを送り、同僚にレビューを受ける場合
  4. レビュアーもまた./openapi3.yamlに対する変更事項が目に見えないため API仕様はそのままなのに実際のAPI実装体は変更されてしまうという不祥事が発生する可能性があります。

Goのコメントの形でAPIドキュメントを作成すると、このような問題をある程度解消することができます。コードとドキュメントが1か所に集まっているため、コードを修正しながらコメントも一緒に更新することができます。このようなコメントを基に、自動的にOpenAPI仕様文書を生成してくれるツールが存在します。

代表的なプロジェクトとしては、Swagがあります。SwagはGoコードのコメントを解析してOpenAPI 2形式のドキュメントを生成してくれます。使い方は簡単です。ハンドラー関数の上に各ライブラリで定めた形式に合わせてコメントを作成すればいいです。

 1// @Summary ユーザー生成
 2// @Description 新しいユーザーを生成します。
 3// @Tags Users
 4// @Accept json
 5// @Produce json
 6// @Param user body models.User true "ユーザー情報"
 7// @Success 200 {object} models.User
 8// @Failure 400 {object} models.ErrorResponse
 9// @Router /users [post]
10func CreateUser(c *gin.Context) {
11    // ...
12}

このようにコメントを作成すると、SwagというCLIはこのコメントを解析してOpenAPI 2文書を生成します。一般的にCI過程でこのような作業が行われ、生成されたOpenAPI仕様のドキュメントはGit Repository、最終ビルド成果物、別途の外部APIドキュメント管理システムに配布され、他のプロジェクトとの協業時に使用されるようになります。

長所:

  • コメントがコードと一緒にあるため、実際のコードとドキュメントの形状が異なる可能性が減ります。
  • 別途のツールや複雑な設定なしにコメントだけで簡単かつ自由にドキュメント化をすることができます。
  • コメントが実際のAPIロジックに影響を与えることはないため、ドキュメントとして公開するのが気が引ける一時的な機能を追加するのに良いです。

短所:

  • コメントの行数が多くなり、単一コードファイルに対する可読性が低下する可能性があります。
  • コメントの形で全てのAPI仕様を表現するのが難しい場合があります。
  • ドキュメントがコードを強制するわけではないため、OpenAPIドキュメントと実際のロジックが一致するという保証はできません。

2. OpenAPI仕様のドキュメントからGoコードを生成

Single source of Truth (SSOT)をGoコードではなくドキュメント側に置く方法も存在します。まさにOpenAPI仕様を先に定義し、定義された内容に基づいてGoコードを生成する方式です。API仕様がコードを生成してくれるため、開発文化的にAPI設計を先に行うことを強制することができ、開発順序的にAPI仕様を定義することが最も先になるため、開発が完了してから見落とした部分を認識し、API仕様の変更とともに全体コードが修正されるという不祥事を早期に防ぐことができるという強みを持っています。

この方式を採用する代表的なプロジェクトとしては、oapi-codegenOpenAPI Generatorが存在します。使い方は簡単です。

  1. OpenAPI仕様に合わせてyamlまたはjsonドキュメントを作成し
  2. CLIを実行すると
  3. それに対応するGo stubコードが生成されます。
  4. これからは、このstubが使用できるように個々のAPIに対する詳細ロジックだけを直接実装すればいいです。

次はoapi-codegenが生成してくれるコードの例です。

1// StrictServerInterface represents all server handlers.
2type StrictServerInterface interface {
3	// ...
4	// Returns all pets
5	// (GET /pets)
6	FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7	// ...
8}

上記のinterfaceを媒介として、oapi-codegenが生成してくれたコードは、query parameters、header、bodyのパースおよびValidationなどのロジックを実行し、interfaceに宣言された適切なmethodを呼び出す構造です。ユーザーは上記のinterfaceに対する実装体のみを実装すれば、API実装に必要な作業が完了することになります。

長所:

  • 仕様が先に出て開発が進むため、複数のチームで協業する場合、業務を並行して進めるのに有利です。
  • 反復性の高い単純作業だった部分に対するコードが自動的に生成されるため、業務効率が向上するとともに、デバッグにも依然として有利です。
  • ドキュメントとコードの形状が常に一致することを保証しやすいです。

短所:

  • OpenAPI仕様自体に無知な状態の場合、初期の学習コストが多少存在します。
  • APIをハンドリングするコードの形状がプロジェクトによって自動的に生成されるため、カスタマイズが必要な場合に対応するのが難しいことがあります。

著者のコメント。 2024年10月現在、OpenAPI Generatorが生成したGoコードは、APIロジックだけでなく全体プロジェクトの形状を強制し、プロジェクトの構造が硬直しているため、実際のProduction環境に必要な様々な機能を追加するには不適切な形のコードを生成しています。この方式を採用される方は、oapi-codegenを使用することをお勧めします。著者は、oapi-codege + echo + StrictServerInterfaceを使用しています。

3. GoコードでOpenAPI仕様文書を生成

数十人、数百人の人々が同じサーバーに対して開発を進めていると、必然的に発生する問題が、個々のAPIごとに統一性が失われる可能性があるということです。直感的な例として、100を超えるAPI Endpointに対する明細を1つのOpenAPI yamlファイルに宣言する場合、当該ファイルは1万行を超える怪物になっているでしょうし、新しいAPI Endpointを宣言する際に必然的に同じモデルを重複して宣言したり、いくつかのフィールドを欠落させたり、慣習に合わないPathネーミングが誕生したりするなど、全体的なAPIの統一性が失われ始めることになります。

このような問題を解決するために、OpenAPI yamlを管理するOwnerを別途置いたり、Linterを開発してCI過程で自動的に検知できるように措置を講じることもできますが、Go言語でDomain-specific language (DSL)を定義し、すべてのAPIが一貫した統一性を持つように強制することができます。

このような技法を使用する代表的なプロジェクトがKubernetesであり(別途ライブラリなしで独自に構築)、go-restfulgoaなどのプロジェクトを使用して試してみることもできます。次はgoaの使用例です。

 1var _ = Service("user", func() {
 2    Method("create", func() {
 3        Payload(UserPayload)
 4        Result(User)
 5        HTTP(func() {
 6            POST("/users")
 7            Response(StatusOK)
 8        })
 9    })
10})

上記のようにコンパイル可能なGoコードを作成すると、POST /users APIに対する実装とドキュメントの定義が同時に完了するという強みを得ることができます。

長所:

  • コードからすべてが出てくるため、プロジェクト全体に対するAPIの一貫性を維持しやすいです。
  • Goの強型システムを活用し、OpenAPI3のすべての機能を活用した場合よりも、より正確で論争のない仕様を得ることができます。

短所:

  • 各フレームワークで定義したDSLを習得する必要があり既存のコードに適用するのは難しい場合があります。
  • フレームワークが提案した規則を強制的に守らなければならないため、自由度および柔軟性が低下する可能性があります。

最後に

各方法には長所と短所があり、プロジェクトの要求事項とチームの好みによって適切な方法を選択することが重要です。いつも最も重要なことは、どのような方式を使うのが良いかではなく、現在自分が置かれた状況に最も適したソリューションは何かを価値判断を行い、開発生産性を高く維持して早く退勤満足のいくワークライフバランスを楽しむことです。

現在2024年10月を基準に文章を作成しましたが、GoとOpenAPI生態系は持続的に発展しているので、この文章を読む時点との間隔を考慮して、各ライブラリおよびプロジェクトの近況とそれらの変更された長所と短所も継続的にフォローアップされることを願います。

幸せなGoライフを! 😘