Go und das OpenAPI-Ökosystem
Einleitung
Beim Entwickeln eines Production Backend-Servers in der Sprache Go gehört zu den ersten Herausforderungen, denen die meisten Entwickler begegnen, die folgende:
API-Dokumentation, wie gehe ich damit um...?
Eine kurze Recherche zeigt, dass es vorteilhaft ist, Dokumente zu erstellen, die der OpenAPI-Spezifikation entsprechen, und man sucht dann natürlich nach Bibliotheken, die mit OpenAPI integriert sind. Doch selbst nach dieser Entscheidung stellt sich das nächste Problem:
Es gibt viele OpenAPI-bezogene Bibliotheken... welche soll ich verwenden...?
Dieses Dokument ist eine kurze Einführung in Bibliotheken, die für Go-Anfänger verfasst wurde, die sich in einer solchen Situation befinden. Es wurde Ende 2024 erstellt, und da sich das Sprachökosystem ständig dynamisch ändert, wird empfohlen, es als Referenz zu nutzen und stets die neuesten Entwicklungen zu verfolgen.
Strategien von Bibliotheken im Umgang mit OpenAPI
Wie Sie vielleicht bereits wissen, ist OpenAPI eine Spezifikation zur klaren Definition und Dokumentation von REST APIs. Sie definiert API-Endpunkte, Anfragen und Antwortformate in YAML- oder JSON-Formaten, was nicht nur Entwicklern, sondern auch der automatischen Codegenerierung für Frontend- und Backend-Komponenten hilft, unnötige Wiederholungen zu reduzieren und kleine menschliche Fehler erheblich zu minimieren.
Um OpenAPI nahtlos in Projekte zu integrieren, verfolgen die Bibliotheken im Go-Ökosystem hauptsächlich die folgenden drei Strategien:
1. Go-Kommentare zur Generierung von OpenAPI-Spezifikationsdokumenten
Eine der Schwierigkeiten bei der Entwicklung von APIs gemäß OpenAPI besteht darin, dass die tatsächliche Dokumentation und der Code, der diese Dokumentation implementiert, oft in separaten Dateien an völlig unterschiedlichen Orten liegen. Dies führt dazu, dass man den Code aktualisiert, aber die Dokumentation vergisst, oder die Dokumentation aktualisiert, aber den Code nicht anpasst, was häufiger vorkommt als erwartet.
Ein einfaches Beispiel:
- Man ändert die Logik für eine API in der Datei
./internal/server/user.go, aber - die eigentliche Dokumentation befindet sich in
./openapi3.yaml, und man vergisst versehentlich, diese Änderung zu aktualisieren. - Wenn man einen Pull Request einreicht und von Kollegen überprüfen lässt, ohne sich des Problems dieser Änderungen bewusst zu sein,
- können die Prüfer die Änderungen an
./openapi3.yamlnicht sehen, was zu dem unglücklichen Umstand führen kann, dass die API-Spezifikation unverändert bleibt, die tatsächliche API-Implementierung sich aber geändert hat.
Das Verfassen der API-Dokumentation in Form von Go-Kommentaren kann dieses Problem teilweise lösen. Da Code und Dokumentation an einem Ort gebündelt sind, können Kommentare gleichzeitig mit Codeänderungen aktualisiert werden. Es gibt Tools, die auf der Grundlage solcher Kommentare automatisch OpenAPI-Spezifikationsdokumente generieren.
Ein bekanntes Projekt in diesem Bereich ist Swag. Swag parst Kommentare im Go-Code und generiert OpenAPI 2-formatierte Dokumente. Die Verwendung ist einfach: Man schreibt Kommentare über die Handler-Funktion gemäß dem von der jeweiligen Bibliothek festgelegten Format.
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 Kommentare auf diese Weise verfasst werden, parst ein CLI-Tool namens Swag diese Kommentare und generiert ein OpenAPI 2-Dokument. Typischerweise wird dieser Vorgang im CI-Prozess durchgeführt, und das generierte OpenAPI-Spezifikationsdokument wird in einem Git Repository, als Teil des finalen Builds oder in einem separaten externen API-Dokumentationssystem bereitgestellt, um die Zusammenarbeit mit anderen Projekten zu ermöglichen.
Vorteile:
- Da die Kommentare zusammen mit dem Code existieren, sinkt die Wahrscheinlichkeit, dass die Dokumentation und der tatsächliche Code voneinander abweichen.
- Die Dokumentation kann einfach und flexibel allein durch Kommentare erstellt werden, ohne zusätzliche Tools oder komplexe Konfigurationen.
- Da Kommentare die tatsächliche API-Logik nicht beeinflussen, eignen sie sich gut, um temporäre Funktionen hinzuzufügen, die nicht zur Veröffentlichung bestimmt sind.
Nachteile:
- Wenn die Anzahl der Kommentarzeilen zunimmt, kann die Lesbarkeit einer einzelnen Codedatei beeinträchtigt werden.
- Es kann schwierig sein, alle API-Spezifikationen in Form von Kommentaren auszudrücken.
- Da die Dokumentation den Code nicht erzwingt, kann nicht garantiert werden, dass das OpenAPI-Dokument und die tatsächliche Logik übereinstimmen.
2. Go-Code aus OpenAPI-Spezifikationsdokumenten generieren
Es gibt auch einen Ansatz, bei dem die Single Source of Truth (SSOT) nicht der Go-Code, sondern die Dokumentation ist. Dies geschieht, indem zuerst die OpenAPI-Spezifikation definiert und dann Go-Code auf der Grundlage dieser Definition generiert wird. Da die API-Spezifikation direkt den Code generiert, kann dies kulturell dazu führen, dass das API-Design zuerst erfolgt. Die Definition der API-Spezifikation ist der erste Schritt im Entwicklungszyklus, was den Vorteil hat, dass fehlende Teile, die erst nach Abschluss der Entwicklung erkannt werden, frühzeitig vermieden werden können, indem eine Änderung der API-Spezifikation und eine vollständige Code-Anpassung verhindert werden.
Zu den repräsentativen Projekten, die diesen Ansatz verfolgen, gehören oapi-codegen und OpenAPI Generator. Die Verwendung ist einfach:
- Man erstellt ein YAML- oder JSON-Dokument gemäß der OpenAPI-Spezifikation.
- Führt das CLI aus.
- Entsprechender Go-Stub-Code wird generiert.
- Nun muss nur noch die detaillierte Logik für die einzelnen APIs implementiert werden, damit der Stub diese verwenden kann.
Das Folgende ist ein Beispiel für Code, der von oapi-codegen generiert wird:
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 generierte Code, der diese Schnittstelle als Grundlage verwendet, führt Logik wie das Parsen von Query-Parametern, Headern und Body sowie die Validierung aus und ruft die entsprechende Methode auf, die in der Schnittstelle deklariert ist. Der Benutzer muss lediglich eine Implementierung für diese Schnittstelle bereitstellen, um die für die API-Implementierung erforderliche Aufgabe abzuschließen.
Vorteile:
- Da die Spezifikation zuerst erstellt und dann die Entwicklung durchgeführt wird, ist dies bei der Zusammenarbeit mehrerer Teams vorteilhaft, um Aufgaben parallel zu bearbeiten.
- Code für wiederholende manuelle Arbeiten wird automatisch generiert, was die Arbeitseffizienz steigert und gleichzeitig für die Fehlersuche weiterhin vorteilhaft ist.
- Es ist leicht sicherzustellen, dass Dokumentation und Code immer konsistent sind.
Nachteile:
- Bei mangelndem Wissen über die OpenAPI-Spezifikation selbst kann die anfängliche Lernkurve etwas steil sein.
- Da die Form des API-Handling-Codes automatisch vom Projekt generiert wird, kann es schwierig sein, Anpassungen vorzunehmen, wenn diese erforderlich sind.
Anmerkung des Autors. Stand Oktober 2024: Der von OpenAPI Generator generierte Go-Code erzwingt nicht nur die API-Logik, sondern auch die gesamte Projektstruktur, was zu einer starren Projektarchitektur führt. Er generiert Code, der für die Implementierung verschiedener Funktionen, die in einer Produktionsumgebung erforderlich sind, ungeeignet ist. Wer diesen Ansatz wählt, dem sei dringend die Verwendung von oapi-codegen empfohlen. Der Autor verwendet oapi-codegen + echo + StrictServerInterface.
3. OpenAPI-Spezifikationsdokumente aus Go-Code generieren
Wenn Dutzende oder Hunderte von Personen an demselben Server entwickeln, treten unweigerlich Probleme auf, bei denen die Konsistenz zwischen einzelnen APIs verloren gehen kann. Ein anschauliches Beispiel: Wenn die Spezifikationen für über 100 API-Endpunkte in einer einzigen OpenAPI-YAML-Datei deklariert werden, wird diese Datei zu einem Monster mit über 10.000 Zeilen. Beim Deklarieren neuer API-Endpunkte kommt es zwangsläufig zu doppelten Deklarationen desselben Modells, zum Weglassen einiger Felder oder zu Pfadbenennungen, die nicht den Konventionen entsprechen, wodurch die Gesamtintegrität der API zu bröckeln beginnt.
Um dieses Problem zu lösen, könnte man einen separaten Owner für die OpenAPI-YAML-Datei ernennen oder einen Linter entwickeln, der Inkonsistenzen während des CI-Prozesses automatisch erkennt. Eine andere Möglichkeit besteht darin, eine Domain-specific language (DSL) in Go zu definieren, um eine konsistente Einheitlichkeit aller APIs zu erzwingen.
Ein repräsentatives Projekt, das diese Technik verwendet, ist Kubernetes (das ohne externe Bibliotheken eigenständig aufgebaut wurde). Projekte wie go-restful und goa können ebenfalls verwendet werden. Das Folgende ist ein Anwendungsbeispiel für 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})
Durch das Schreiben von kompilierbarem Go-Code wie oben kann die Implementierung und Definition der Dokumentation für die POST /users-API gleichzeitig abgeschlossen werden, was einen Vorteil darstellt.
Vorteile:
- Da alles aus dem Code entsteht, ist es einfach, eine API-Konsistenz für das gesamte Projekt aufrechtzuerhalten.
- Durch die Nutzung des stark typisierten Systems von Go können präzisere und eindeutigere Spezifikationen erzielt werden, als wenn alle Funktionen von OpenAPI3 genutzt werden.
Nachteile:
- Die in jedem Framework definierte DSL muss erlernt werden, und die Anwendung auf bestehenden Code kann schwierig sein.
- Da die vom Framework vorgeschlagenen Regeln erzwungen werden müssen, können Freiheit und Flexibilität eingeschränkt sein.
Zusammenfassend
Jede Methode hat Vor- und Nachteile, und es ist wichtig, die geeignete Methode basierend auf den Projektanforderungen und den Präferenzen des Teams zu wählen. Das Wichtigste ist nicht, welche Methode die beste ist, sondern eine Wertentscheidung zu treffen, welche Lösung für die aktuelle Situation am besten geeignet ist, um die Entwicklungsproduktivität zu steigern und somit schnell Feierabend zu machen und eine zufriedenstellende Work-Life-Balance zu genießen.
Obwohl dieser Artikel im Oktober 2024 verfasst wurde, entwickeln sich das Go- und OpenAPI-Ökosystem ständig weiter. Berücksichtigen Sie daher die Zeitspanne bis zu dem Zeitpunkt, an dem Sie diesen Artikel lesen, und verfolgen Sie weiterhin die aktuellen Entwicklungen und die veränderten Vor- und Nachteile der einzelnen Bibliotheken und Projekte.
Ein glückliches Go-Leben wünsche ich Ihnen! 😘