logo hsb.horse
← Retour au blog

Blog

Politique de base lors de la mise en œuvre d'un serveur API avec Golang

J'ai organisé mes propres politiques de base lors de l'écriture d'un serveur API dans Go, y compris un binaire unique + cobra, la concentration sur les opérations, la séparation HTTP/CLI, la gestion sécurisée des erreurs et l'arrêt progressif.

Publié:

Lorsque vous commencez à écrire un serveur API dans Go, vous souhaitez d’abord choisir un routeur ou un framework. Mais ce n’est pas ce que vous devriez vraiment décider en premier. Les premières choses que vous souhaitez corriger sont le modèle d’exécution, la répartition des responsabilités, les limites d’erreur et l’ordre d’arrêt.

Le routeur peut être changé ultérieurement. Une conception avec un ordre d’arrêt brisé est difficile à corriger plus tard. Je pense qu’il serait plus facile de résoudre ce problème en premier.

Cet article n’est pas une théorie générale de Go, mais plutôt un mémo de politique de base pour vous aider à éviter toute confusion la prochaine fois que vous implémenterez un serveur API.

Corrigez d’abord la politique

ArticlePolitique
Produit finalCommencez avec un seul binaire de cobra
Démarrer le serveur HTTPCommencez par la sous-commande serve comme myapp serve
Unité centraleCentre Operation[In, Out]
transportsHTTP et CLI sont séparés en tant qu’adaptateurs
cadreNe laissez pas couler chi / gin / Echo en dehors du transport/http
transversaleLe middleware HTTP et l’intercepteur d’opération sont séparés
erreurTraduisez en toute sécurité les erreurs internes avec des limites
arrêtAu lieu de simplement appeler server.Shutdown, définissez l’ordre d’arrêt sur deterministic

Bref, la structure n’est pas centrée autour du framework HTTP, mais centrée autour des opérations et des cycles de vie.

Le serveur HTTP est également traité comme CLI

Le serveur HTTP est en fait démarré à l’aide de la CLI. Dans ce cas, il serait plus facile de comprendre si vous les regroupiez dans cobra depuis le début et les combiniez en un seul binaire.

Le système de commande public recommandé est le suivant.

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

Je pense qu’une approche en deux étapes comme server run est redondante. serve complète le sens et réduit la quantité de saisie.

La raison de privilégier le binaire unique est simple.

  1. Un seul document est requis
  2. La lecture de la configuration, l’enregistreur, l’initialisation de la base de données et la gestion des erreurs peuvent être effectués dans un seul système.
  3. Bootstrap facile à partager entre le serveur HTTP et la CLI
  4. Même si vous souhaitez convertir en 2 binarisation plus tard, vous pouvez simplement augmenter légèrement cmd/.

D’un autre côté, choisir des binaires séparés dès le début ne peut être fait qu’après avoir compris la raison, comme vouloir rendre le conteneur de serveur extrêmement petit, avoir des dépendances et des privilèges significativement différents entre le serveur et le cli, ou vouloir séparer la fréquence des versions.

Le cœur de l’implémentation se trouve dans Operation

Si vous concevez autour du gestionnaire HTTP, la commodité des routeurs et des frameworks sera profondément impliquée. Ensuite, lorsque vous souhaitez étendre la CLI, vous avez tendance à réécrire le même processus.

Il vaut mieux le centrer sur Operation.

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

Qu’il s’agisse de users get ou users create, appelez simplement la même opération pour HTTP et CLI. HTTP est un adaptateur qui convertit la requête/réponse, et CLI est un adaptateur qui convertit flag/stdin/stdout.

Si vous faites cela, le cœur de l’application ne changera pas même si vous augmentez le nombre de transports.

Le framework est limité à transport/http

Que ce soit pour utiliser chi, gin ou Echo est une question de transport. Il ne s’agit pas de domaines ou d’opérations.

Ce que vous voulez faire lors du choix d’un routeur n’est pas de décider de la responsabilité du serveur API, mais simplement de décider comment utiliser HTTP. Par conséquent, les différences de cadre sont limitées à internal/transport/http.

L’image ressemble à ceci.

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

Si vous voyez chi ou gin.Context à partir de operation, il y a probablement une fuite. Si cela commence à fuir, il ne s’agit pas de changer le framework, il s’agit de réécrire l’intégralité de l’application.

Ne mélangez pas le middleware HTTP et l’intercepteur d’opérations

Il sera plus facile d’y réfléchir séparément.

CoucheQue mettre
Intergiciel HTTPÉléments spécifiques à HTTP tels que l’identifiant de la demande, le journal d’accès, la récupération, l’authentification, CORS
Opération intercepteurvalidation, authentification, délai d’attente, métriques, journalisation par cas d’utilisation

Le middleware HTTP est pratique pour le transport. L’opération intercepteur est destinée à la commodité de l’application. Même avec la même journalisation, la signification de l’emplacement est différente.

La combinaison de ces deux éléments tend à aboutir à une conception HTTP fermée. Au moment où vous ajoutez la CLI, vous devrez réécrire la validation et l’authentification avec des implémentations distinctes.

Convertissez l’erreur en toute sécurité avec la limite

N’envoyez pas d’erreurs internes directement vers la réponse HTTP ou la sortie standard CLI. Il est plus facile d’établir des règles ici dès le début.

Dans votre application, utilisez une erreur contenant au moins les quatre éléments suivants.

ArticleRôle
CodeClassifications telles que bad_request et not_found
PublicMention pouvant être restituée au client
InternalErreur détaillée interne
MetaJournaux et informations auxiliaires

En HTTP, cela se traduit par code d’état et JSON. En CLI, cela se traduit par stderr et code de sortie. L’important est de faire la translation à la frontière. Ne commencez pas à penser au statut HTTP du côté des opérations.

Un arrêt progressif doit être conçu, y compris l’ordre d’arrêt

C’est un point très important dans la mise en œuvre du serveur API de Go. Il n’est pas traité comme un « arrêt progressif pris en charge » simplement en appelant srv.Shutdown(ctx).

Dans la documentation officielle net/http, Shutdown ferme l’écouteur, arrête la nouvelle réception, ferme la connexion inactive et attend que la connexion active revienne au repos. D’un autre côté, ListenAndServe renvoie ErrServerClosed après avoir appelé Shutdown ou Close. En d’autres termes, si vous terminez main uniquement en regardant le retour, le processus peut se terminer alors qu’il est arrêté.

De plus, Shutdown n’attend pas les connexions détournées. Utilisez RegisterOnShutdown si nécessaire et utilisez BaseContext si vous souhaitez propager l’arrêt du côté du gestionnaire. Si vous convertissez le signal en contexte à l’aide de os/signal’s NotifyContext, il sera plus facile d’aligner le point de départ du traitement de terminaison.

Si vous utilisez Kubernetes, l’ordre stop est encore plus important. Pod lifecycle donne au Pod une période de résiliation gracieuse, et container lifecycle hooks continue son compte à rebours tout en exécutant PreStop. Par conséquent, si vous l’arrêtez négligemment, le temps s’écoulera avant la fin de drain.

Ma politique de base est cet ordre.

CommanderChoses à faire
1Recevoir le signal de terminaison sur signal.NotifyContext
2Arrêtez les nouveaux afflux en supprimant readiness
3Fermer la file d’attente entrée consommateur/cron/travailleur en arrière-plan
4drain pendant un moment et attendez que l’équilibreur de charge soit reflété
5Propager l’annulation du contexte au gestionnaire
6Appelez server.Shutdown(timeoutCtx)
7Attendez la fin de la tâche en vol le WaitGroup / errgroup
8Fermez enfin DB/cache/broker producteur
9Utilisez server.Close() en dernier recours uniquement lorsque le délai d’attente est dépassé

Je pense que le cœur de cette discussion n’est pas tellement spécifique à Go, mais plutôt que vous devriez concevoir vos séquences d’arrêt pour qu’elles soient déterministes.

Il existe deux types de health

Ici aussi, il est facile de se tromper.

NomSignification
myapp healthCLI pour diagnostiquer directement les dépendances localement
GET /api/healthPoint de terminaison d’intégrité HTTP pour le démarrage du serveur

Le même health mais des rôles différents. Le côté CLI est similaire aux diagnostics et le côté HTTP est similaire à readiness / liveness. En particulier, il est plus sûr de concevoir l’état de santé côté HTTP pour qu’il renvoie 503 lors de l’arrêt.

Politique de répertoire minimale

C’est suffisant pour le squelette initial.

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

Gardez cmd/myapp mince. L’initialisation va à bootstrap, les cas d’utilisation à operation et HTTP et CLI à transport. Même si vous souhaitez convertir en 2 binarisation plus tard, si vous utilisez cette méthode de découpe, vous pouvez facilement simplement ajouter cmd/myapp-server.

Phrase que vous souhaitez conserver

Lors de la mise en œuvre d’un serveur API Go, la première chose à décider n’est pas le cadre, mais le modèle d’exécution, la répartition des responsabilités, les limites d’erreur et l’ordre d’arrêt.

Cette seule phrase suffit généralement. Vous pouvez décider d’utiliser chi ou Echo après cela.

résumé

Ma politique de base est la suivante. Tout d’abord, combinez-le en un seul binaire de cobra et démarrez le serveur HTTP avec serve. Le centre sera placé sur Operation, et HTTP et CLI seront séparés en tant qu’adaptateurs. Le cadre se limite à transport/http. L’erreur est convertie en toute sécurité à l’aide de la limite. L’arrêt ne se termine pas à server.Shutdown, mais l’ordre est conçu pour inclure readiness, drain, l’arrêt du travailleur et le nettoyage des ressources.

Si vous faites cela, la conception restera cohérente même si vous modifiez le cadre ultérieurement, ajoutez une CLI ou augmentez le nombre de travailleurs.

Référence