logo hsb.horse
← Zur Blog-Übersicht

Blog

Grundlegende Richtlinie bei der Implementierung eines API-Servers mit Golang

Beim Schreiben eines API-Servers in Go habe ich meine eigenen grundlegenden Richtlinien organisiert, einschließlich Single Binary + Cobra, Operationsfokus, HTTP/CLI-Trennung, sichere Fehlerbehandlung und ordnungsgemäßes Herunterfahren.

Veröffentlicht:

Wenn Sie mit dem Schreiben eines API-Servers in Go beginnen, möchten Sie zunächst einen Router oder ein Framework auswählen. Aber das ist nicht das, was Sie wirklich zuerst entscheiden sollten. Die ersten Dinge, die Sie korrigieren möchten, sind das Ausführungsmodell, die Aufteilung der Verantwortlichkeiten, Fehlergrenzen und die Stoppreihenfolge.

Der Router kann später geändert werden. Ein Entwurf mit einer fehlerhaften Stoppreihenfolge lässt sich später nur schwer reparieren. Ich denke, es wäre einfacher, das zuerst zu beheben.

Bei diesem Artikel handelt es sich nicht um eine allgemeine Go-Theorie, sondern vielmehr um ein grundlegendes Richtlinienmemo, das Ihnen helfen soll, Verwirrung zu vermeiden, wenn Sie das nächste Mal einen API-Server implementieren.

Korrigieren Sie zuerst die Richtlinie

ArtikelPolitik
EndproduktBeginnen Sie mit einer einzelnen Binärdatei von cobra
HTTP-Server startenBeginnen Sie mit dem Unterbefehl serve wie myapp serve
ZentraleinheitMitte Operation[In, Out]
TransportHTTP und CLI werden als Adapter
RahmenLassen Sie chi / gin / Echo nicht außerhalb von transport/http
querschnittlichHTTP-Middleware und Operation Interceptor sind getrennt
FehlerInterne Fehler sicher mit Grenzen übersetzen
HerunterfahrenAnstatt nur server.Shutdown aufzurufen, legen Sie die Reihenfolge beim Herunterfahren auf deterministisch

Kurz gesagt, die Struktur konzentriert sich nicht auf das HTTP-Framework, sondern auf Vorgänge und Lebenszyklen.

HTTP-Server wird auch als CLI behandelt

Der eigentliche Start des HTTP-Servers erfolgt über die CLI. In diesem Fall wäre es einfacher zu verstehen, wenn Sie sie von Anfang an in cobra gruppieren und zu einer Binärdatei kombinieren würden.

Das empfohlene öffentliche Befehlssystem ist wie folgt.

Terminal window
./myapp serve
./myapp health
./myapp users get 123
./myapp users create --name alice

Ich halte einen zweistufigen Ansatz wie server run für überflüssig. serve vervollständigt die Bedeutung und reduziert den Tippaufwand.

Der Grund für die Bevorzugung einzelner Binärdateien ist einfach.

  1. Es ist nur ein Handout erforderlich
  2. Das Auslesen der Konfiguration, die Protokollierung, die DB-Initialisierung und die Fehlerbehandlung können in einem System erfolgen.
  3. Einfach zu teilender Bootstrap zwischen HTTP-Server und CLI
  4. Auch wenn Sie später auf 2 Binärisierung umstellen möchten, können Sie cmd/ einfach leicht erhöhen.

Andererseits kann die Auswahl separater Binärdateien von Anfang an nur erfolgen, wenn Sie den Grund verstanden haben, z. B. den Wunsch, den Servercontainer extrem klein zu machen, deutlich unterschiedliche Abhängigkeiten und Berechtigungen zwischen Server und CLI zu haben oder die Häufigkeit der Veröffentlichungen zu trennen.

Der Kern der Implementierung liegt in Operation

Wenn Sie den HTTP-Handler beim Entwurf berücksichtigen, wird die Benutzerfreundlichkeit von Routern und Frameworks stark berücksichtigt. Wenn Sie dann die CLI erweitern möchten, neigen Sie dazu, denselben Prozess erneut zu schreiben.

Es ist besser, es auf Operation zu zentrieren.

type Operation[In any, Out any] interface {
Execute(ctx context.Context, in In) (Out, error)
}

Unabhängig davon, ob es sich um users get oder users create handelt, rufen Sie einfach denselben Vorgang für HTTP und CLI auf. HTTP ist ein Adapter, der Anfrage/Antwort konvertiert, und CLI ist ein Adapter, der Flag/stdin/stdout konvertiert.

Wenn Sie dies tun, ändert sich der Kern der Anwendung nicht, auch wenn Sie die Anzahl der Transporte erhöhen.

Framework ist auf transport/http beschränkt

Ob chi, gin oder Echo verwendet werden soll, ist eine Frage des Transports. Es geht nicht um Domänen oder Operationen.

Bei der Auswahl eines Routers möchten Sie nicht die Verantwortung des API-Servers festlegen, sondern lediglich entscheiden, wie HTTP verwendet wird. Daher sind Framework-Unterschiede auf internal/transport/http beschränkt.

Das Bild sieht so aus.

internal/
bootstrap/
operation/
interceptor/
transport/
http/
cli/
domain/
infra/
apperr/

Wenn Sie chi oder gin.Context von operation sehen, liegt wahrscheinlich ein Leck vor. Wenn dies zu lecken beginnt, geht es nicht darum, das Framework zu ändern, sondern darum, die gesamte App neu zu schreiben.

Mischen Sie nicht HTTP-Middleware und Operation Interceptor

Es wird einfacher sein, darüber separat nachzudenken.

SchichtWas soll ich sagen
HTTP-MiddlewareHTTP-spezifische Dinge wie Anforderungs-ID, Zugriffsprotokoll, Wiederherstellung, Authentifizierung, CORS
Operation AbfangjägerValidierung, Authentifizierung, Timeout, Metriken, Protokollierung pro Anwendungsfall

HTTP-Middleware ist praktisch für den Transport. Der Operation Interceptor dient der Anwendungsfreundlichkeit. Selbst bei gleicher Protokollierung ist die Bedeutung des Standorts unterschiedlich.

Die Kombination dieser beiden führt tendenziell zu einem HTTP-geschlossenen Design. Sobald Sie CLI hinzufügen, müssen Sie Validierung und Authentifizierung mit separaten Implementierungen neu schreiben.

Fehler sicher mit Grenze konvertieren

Geben Sie interne Fehler nicht direkt an die HTTP-Antwort oder die CLI-Standardausgabe aus. Hier ist es einfacher, von Anfang an Regeln aufzustellen.

Verwenden Sie in Ihrer App einen Fehler, der mindestens die folgenden vier Elemente enthält.

ArtikelRolle
CodeKlassifizierungen wie bad_request und not_found
PublicFormulierungen, die an den Kunden zurückgegeben werden können
InternalInterner detaillierter Fehler
MetaProtokolle und Zusatzinformationen

In HTTP wird es in Statuscode und JSON übersetzt. In der CLI wird es in stderr und Exit-Code übersetzt. Wichtig ist, dass die Übersetzung an der Grenze erfolgt. Denken Sie nicht über den HTTP-Status auf der Betriebsseite nach.

Es sollte ein ordnungsgemäßes Herunterfahren einschließlich der Abschaltreihenfolge geplant werden

Dies ist ein sehr wichtiger Punkt in der API-Server-Implementierung von Go. Es wird nicht allein durch den Aufruf von srv.Shutdown(ctx) als „anständiges Herunterfahren unterstützt“ behandelt.

In offizieller Dokumentation zu net/http schließt Shutdown den Listener, stoppt den neuen Empfang, schließt die inaktive Verbindung und wartet darauf, dass die aktive Verbindung wieder in den Leerlauf wechselt. Andererseits gibt ListenAndServe nach dem Aufruf von Shutdown oder Close ErrServerClosed zurück. Mit anderen Worten: Wenn Sie main nur durch einen Blick auf die Rückgabe beenden, kann der Prozess enden, während er gestoppt ist.

Darüber hinaus wartet Shutdown nicht auf gekaperte Verbindungen. Verwenden Sie bei Bedarf RegisterOnShutdown und BaseContext, wenn Sie das Herunterfahren an die Handler-Seite weitergeben möchten. Wenn Sie das Signal mithilfe von os/signal’s NotifyContext in einen Kontext konvertieren, ist es einfacher, den Startpunkt für die Beendigungsverarbeitung auszurichten.

Wenn Sie Kubernetes verwenden, ist die Stoppreihenfolge noch wichtiger. Pod-Lebenszyklus gibt dem Pod eine ordnungsgemäße Beendigungsfrist und Container-Lebenszyklus-Hooks setzt seinen Countdown fort, während PreStop ausgeführt wird. Wenn Sie es daher schlampig stoppen, läuft die Zeit ab, bevor drain beendet ist.

Meine Grundpolitik ist diese Reihenfolge.

BestellenUnternehmungen
1Beendigungssignal auf signal.NotifyContext
2Stoppen Sie den Neuzufluss, indem Sie readiness
3Schließen Sie den Warteschlangen-Konsumenten-/Cron-/Hintergrund-Worker-Eingang
4drain für eine Weile und warten Sie, bis der Load Balancer angezeigt wird
5Kontextabbruch an Handler weitergeben
6Rufen Sie server.Shutdown(timeoutCtx)
7Warten Sie auf den Abschluss des laufenden Auftrags am WaitGroup / errgroup
8Schließen Sie schließlich DB/Cache/Broker Producer
9Verwenden Sie server.Close() nur als letzten Ausweg, wenn die Zeitüberschreitung überschritten ist

Ich denke, der Kern dieser Diskussion ist nicht so sehr Go-spezifisch, sondern vielmehr, dass Sie Ihre Stoppsequenzen so gestalten sollten, dass sie [deterministisch] sind (/glossary/deterministic/).

Es gibt zwei Arten von health

Auch hier kommt man leicht durcheinander.

NameBedeutung
myapp healthCLI zur direkten lokalen Diagnose von Abhängigkeiten
GET /api/healthHTTP-Integritätsendpunkt zum Starten des Servers

Das gleiche health, aber unterschiedliche Rollen. Die CLI-Seite ähnelt der Diagnose und die HTTP-Seite ähnelt readiness / liveness. Insbesondere ist es sicherer, den Zustand der HTTP-Seite so zu gestalten, dass beim Herunterfahren 503 zurückgegeben wird.

Minimale Verzeichnisrichtlinie

Das reicht für das Grundgerüst.

cmd/
myapp/
internal/
apperr/
bootstrap/
domain/
infra/
interceptor/
operation/
health/
users/
transport/
cli/
http/
version/

Halten Sie cmd/myapp dünn. Die Initialisierung geht an bootstrap, Anwendungsfälle an operation und HTTP und CLI an transport. Auch wenn Sie später auf 2-Binarisierung umstellen möchten, können Sie bei Verwendung dieser Schnittmethode ganz einfach cmd/myapp-server hinzufügen.

Satz, den Sie behalten möchten

Bei der Implementierung eines Go-API-Servers muss zunächst nicht das Framework, sondern das Ausführungsmodell, die Aufteilung der Verantwortlichkeiten, die Fehlergrenzen und die Stoppreihenfolge festgelegt werden.

Dieser eine Satz reicht normalerweise aus. Danach können Sie entscheiden, ob Sie chi oder Echo verwenden möchten.

Zusammenfassung

Meine Grundpolitik ist folgende. Kombinieren Sie es zunächst zu einer einzigen Binärdatei von cobra und starten Sie den HTTP-Server mit serve. Das Zentrum wird auf Operation platziert und HTTP und CLI werden als Adapter getrennt. Das Framework ist auf transport/http beschränkt. Der Fehler wird mithilfe der Grenze sicher konvertiert. Das Herunterfahren endet nicht bei server.Shutdown, aber die Reihenfolge soll Bereitschaft, entleeren, Arbeitsstopp und Ressourcenbereinigung umfassen.

Wenn Sie dies tun, bleibt das Design konsistent, auch wenn Sie später das Framework ändern, eine CLI hinzufügen oder die Anzahl der Worker erhöhen.

Referenz