Go und das OpenAPI-Ökosystem

Einleitung
Wenn man einen Production-Backend-Server mit der Sprache Go entwickelt, stößt fast jeder Entwickler früher oder später auf eine der ersten großen Herausforderungen, die sich wie folgt formulieren lässt.
API-Dokumentation, wie soll ich das machen...?
Eine kurze Recherche zu diesem Thema zeigt, dass es vorteilhaft ist, eine Dokumentation gemäß der OpenAPI-Spezifikation zu erstellen, und man sucht daraufhin natürlich nach Bibliotheken, die sich in OpenAPI integrieren lassen. Doch selbst nach dieser Entscheidung stellt sich das nächste Problem.
Es gibt so viele OpenAPI-Bibliotheken.. welche soll ich verwenden...?
Dieser Text ist eine knappe Einführung in Bibliotheken, die ich für Go-Einsteiger verfasst habe, die sich in dieser Situation befinden. Der Text wurde zum Stand Ende 2024 geschrieben, und da sich das Ökosystem einer Sprache ständig wandelt, empfehle ich, ihn als Orientierung zu verwenden und stets auch die aktuellen Entwicklungen zu verfolgen.
Strategien der Bibliotheken im Umgang mit OpenAPI
Wie Sie vermutlich bereits wissen, ist OpenAPI eine Spezifikation zur eindeutigen Definition und Dokumentation von REST-APIs. Die Endpunkte, Anfragen und Antwortformate einer API werden im YAML- oder JSON-Format definiert. Dies trägt nicht nur für Entwickler, sondern auch für Frontend- und Backend-Code erheblich dazu bei, die Codegenerierung zu automatisieren, unnötige Wiederholungen zu reduzieren und kleine menschliche Fehler zu vermeiden.
Um OpenAPI auf natürliche Weise mit einem Projekt zu verbinden, verfolgen die Bibliotheken im Go-Ökosystem grob gesagt die folgenden drei Strategien.
1. Kombination von Go-Kommentaren zu einer OpenAPI-Spezifikationsdatei
Eine Schwierigkeit bei der Entwicklung einer API gemäß OpenAPI besteht darin, dass die eigentliche Dokumentation und der Code, der sie implementiert, in getrennten Dateien an völlig unterschiedlichen Orten liegen. Dadurch kommt es erstaunlich häufig vor, dass der Code aktualisiert, die Dokumentation aber nicht angepasst wird, oder dass die Dokumentation aktualisiert wird, der Code jedoch nicht.
Ein einfaches Beispiel:
- Sie ändern die Logik einer API in der Datei
./internal/server/user.go. - Die eigentliche Dokumentation befindet sich in
./openapi3.yaml, und die Änderung dort könnten Sie versehentlich vergessen. - Wenn Sie ein Pull Request einreichen, ohne dieses Problem zu bemerken, und Ihre Kollegen um ein Review bitten,
- können auch die Reviewer die Änderungen in
./openapi3.yamlnicht sehen. Dadurch kann der unerfreuliche Fall eintreten, dass die API-Spezifikation unverändert bleibt, die tatsächliche Implementierung der API jedoch verändert wird.
Wenn Sie die API-Dokumentation in Form von Go-Kommentaren schreiben, lässt sich dieses Problem bis zu einem gewissen Grad lösen. Da Code und Dokumentation an einem Ort zusammenliegen, können Sie die Kommentare beim Ändern des Codes gleich mit aktualisieren. Auf Grundlage dieser Kommentare existieren Werkzeuge, die die OpenAPI-Spezifikationsdatei automatisch generieren.
Ein repräsentatives Projekt ist Swag. Swag analysiert die Kommentare im Go-Code und erzeugt daraus eine Dokumentation im Format OpenAPI 2. Die Verwendung ist einfach: Sie schreiben über der Handler-Funktion Kommentare im Format, das die jeweilige Bibliothek vorgibt.
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}
Wenn Sie die Kommentare so schreiben, analysiert das CLI-Werkzeug Swag diese Kommentare und erzeugt daraus eine OpenAPI-2-Dokumentation. Üblicherweise wird dieser Vorgang in der CI-Pipeline ausgeführt, und die erzeugte OpenAPI-Spezifikation wird im Git-Repository, im finalen Build-Artefakt oder in einem separaten externen System zur Verwaltung von API-Dokumentationen bereitgestellt, sodass sie bei der Zusammenarbeit mit anderen Projekten verwendet werden kann.
Vorteile:
- Da die Kommentare zusammen mit dem Code vorliegen, sinkt die Wahrscheinlichkeit, dass sich tatsächlicher Code und Dokumentation auseinanderentwickeln.
- Ohne zusätzliche Werkzeuge oder komplexe Konfiguration können Sie einfach und flexibel nur mit Kommentaren dokumentieren.
- Da die Kommentare keinen Einfluss auf die eigentliche API-Logik haben, eignet es sich gut, temporäre Funktionen hinzuzufügen, deren Dokumentation nach außen zu belastend wäre.
Nachteile:
- Durch die wachsende Anzahl an Kommentarzeilen kann die Lesbarkeit einer einzelnen Code-Datei sinken.
- Es kann schwierig sein, alle API-Spezifikationen in Form von Kommentaren darzustellen.
- Da die Dokumentation den Code nicht erzwingt, kann nicht garantiert werden, dass die OpenAPI-Dokumentation und die tatsächliche Logik übereinstimmen.
2. Erzeugung von Go-Code aus einer OpenAPI-Spezifikationsdatei
Es gibt auch den Ansatz, die Single Source of Truth (SSOT) nicht im Go-Code, sondern in der Dokumentation zu verorten. Dabei wird zunächst die OpenAPI-Spezifikation definiert und anschließend auf dieser Grundlage Go-Code generiert. Da die API-Spezifikation den Code erzeugt, lässt sich kulturell im Entwicklungsprozess erzwingen, dass der API-Entwurf zuerst erfolgt. Da die Definition der API-Spezifikation am Anfang des Entwicklungsprozesses steht, besteht die Stärke dieses Ansatzes darin, frühzeitig zu verhindern, dass man erst nach Abschluss der Entwicklung Lücken bemerkt und anschließend den gesamten Code zusammen mit einer Änderung der API-Spezifikation überarbeiten muss.
Repräsentative Projekte, die diesen Ansatz verfolgen, sind oapi-codegen und OpenAPI Generator. Die Verwendung ist einfach:
- Schreiben Sie ein YAML- oder JSON-Dokument gemäß der OpenAPI-Spezifikation,
- führen Sie das CLI aus,
- daraufhin wird entsprechender Go-Stub-Code erzeugt.
- Anschließend müssen Sie nur noch die detaillierte Logik der einzelnen APIs implementieren, damit der Stub verwendet werden kann.
Im Folgenden ist ein Beispiel für Code aufgeführt, den oapi-codegen generiert.
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}
Der von oapi-codegen auf Grundlage dieses Interfaces generierte Code übernimmt die Verarbeitung von Query-Parametern, Headern und Bodies sowie die Validierung und ruft die passende im Interface deklarierte Methode auf. Als Anwender müssen Sie lediglich eine Implementierung dieses Interfaces schreiben, dann ist die für die API-Implementierung nötige Arbeit abgeschlossen.
Vorteile:
- Da die Spezifikation zuerst entsteht und die Entwicklung danach erfolgt, ist es bei Zusammenarbeit mehrerer Teams vorteilhaft, Aufgaben parallel zu bearbeiten.
- Code, der bisher bei repetitiver Routinearbeit geschrieben wurde, wird automatisch generiert, wodurch die Arbeitseffizienz steigt, während das Debugging weiterhin erleichtert bleibt.
- Es ist leicht sicherzustellen, dass Dokumentation und Code stets übereinstimmen.
Nachteile:
- Wenn man mit der OpenAPI-Spezifikation selbst nicht vertraut ist, gibt es eine gewisse anfängliche Einarbeitungszeit.
- Da die Struktur des Codes zur Behandlung der API durch das Projekt automatisch erzeugt wird, kann es bei Bedarf an Anpassungen schwierig sein, darauf zu reagieren.
Kommentar des Autors. Stand Oktober 2024 erzeugt der OpenAPI Generator Go-Code, der nicht nur die API-Logik, sondern die gesamte Projektstruktur vorgibt. Da die Struktur dadurch starr ist, eignet sich dieser Code kaum für das Hinzufügen der vielfältigen Funktionen, die in einer Production-Umgebung benötigt werden. Wer sich für diesen Ansatz entscheidet, dem wird dringend empfohlen, oapi-codegen zu verwenden. Der Autor verwendet oapi-codegen + echo + StrictServerInterface.
3. Erzeugung von OpenAPI-Spezifikationsdokumenten aus Go-Code
Wenn Dutzende oder Hunderte Personen gemeinsam an demselben Server entwickeln, tritt zwangsläufig das Problem auf, dass die Einheitlichkeit einzelner APIs verloren gehen kann. Ein anschauliches Beispiel: Wenn die Spezifikation von mehr als 100 API-Endpunkten in einer einzigen OpenAPI-YAML-Datei deklariert wird, wird diese Datei zu einem Monster mit über 10.000 Zeilen. Beim Hinzufügen eines neuen API-Endpunkts werden dann zwangsläufig dieselben Modelle mehrfach deklariert, einzelne Felder vergessen oder Pfadnamen gewählt, die nicht den Konventionen entsprechen, und die Einheitlichkeit der gesamten API beginnt zu bröckeln.
Um dieses Problem zu lösen, könnte man eine eigene Owner-Rolle für die Verwaltung der OpenAPI-YAML einrichten oder einen Linter entwickeln, der im CI-Prozess automatisch Abweichungen erkennt. Man kann jedoch auch eine Domain-specific Language (DSL) in Go definieren und so erzwingen, dass alle APIs eine einheitliche Konsistenz aufweisen.
Ein repräsentatives Projekt, das diese Technik verwendet, ist Kubernetes (ohne zusätzliche Bibliothek, mit eigener Implementierung). Außerdem kann man Projekte wie go-restful oder goa ausprobieren. Im Folgenden ist ein Anwendungsbeispiel für goa gezeigt.
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})
Wenn Sie auf diese Weise kompilierbaren Go-Code schreiben, erhalten Sie den Vorteil, dass die Implementierung der API POST /users und die Definition der Dokumentation gleichzeitig abgeschlossen sind.
Vorteile:
- Da alles aus dem Code entsteht, lässt sich die API-Konsistenz im gesamten Projekt leicht wahren.
- Durch die Nutzung des stark typisierten Systems von Go erhält man eine präzisere und unstrittigere Spezifikation als bei der Nutzung aller Funktionen von OpenAPI 3.
Nachteile:
- Man muss die vom jeweiligen Framework definierte DSL erlernen, und die Anwendung auf bestehenden Code kann schwierig sein.
- Da die vom Framework vorgeschlagenen Regeln zwingend befolgt werden müssen, kann die Freiheit und Flexibilität eingeschränkt sein.
Abschließende Worte
Jede Methode hat ihre Vor- und Nachteile, und es ist wichtig, je nach Anforderungen des Projekts und den Vorlieben des Teams die passende Methode zu wählen. Entscheidend ist nicht, welche Methode die beste ist, sondern dass man bewertet, welche Lösung zur eigenen Situation am besten passt, die Entwicklungsproduktivität hoch hält und so früh Feierabend sowie eine erfreuliche Work-Life-Balance genießen kann.
Auch wenn dieser Text zum Stand Oktober 2024 verfasst wurde, entwickeln sich das Go- und das OpenAPI-Ökosystem kontinuierlich weiter. Bitte berücksichtigen Sie daher den zeitlichen Abstand zum Lesezeitpunkt und verfolgen Sie auch die aktuellen Entwicklungen der einzelnen Bibliotheken und Projekte sowie deren veränderte Vor- und Nachteile fortlaufend.
Viel Freude mit Go und happy coding~ 😘