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 ドキュメントを記述すると、このような問題をある程度解消できます。コードとドキュメントが一箇所にまとまっているため、コードを修正する際にコメントも一緒に更新できます。このようなコメントを基に 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-codegen と OpenAPI 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-codegen + echo + StrictServerInterface を使用しています。

3. Go コードから OpenAPI スペックのドキュメントを生成

数十、数百人が同じサーバーについて開発を進めていると、必然的に発生する問題として、個々の API ごとに統一性が損なわれる可能性があります。直感的な例を挙げると、100 を超える API Endpoint の仕様を一つの OpenAPI yaml ファイルに宣言した場合、そのファイルは 1 万行を超える怪物になっているでしょう。そして新しい API Endpoint を宣言する過程で、同じモデルを必然的に重複して宣言したり、一部のフィールドを欠落させたり、規約に合わないパスの命名が生まれたりと、API 全体の統一性が崩れ始めます。

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

この手法を用いる代表的なプロジェクトが Kubernetes であり (別途のライブラリなしで独自に構築)、go-restful、goa などのプロジェクトを使ってみることもできます。以下は 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 ライフをお過ごしください~ 😘