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
| Article | Politique |
|---|---|
| Produit final | Commencez avec un seul binaire de cobra |
| Démarrer le serveur HTTP | Commencez par la sous-commande serve comme myapp serve |
| Unité centrale | Centre Operation[In, Out] |
| transports | HTTP et CLI sont séparés en tant qu’adaptateurs |
| cadre | Ne laissez pas couler chi / gin / Echo en dehors du transport/http |
| transversale | Le middleware HTTP et l’intercepteur d’opération sont séparés |
| erreur | Traduisez en toute sécurité les erreurs internes avec des limites |
| arrêt | Au 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.
./myapp serve./myapp health./myapp users get 123./myapp users create --name aliceJe 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.
- Un seul document est requis
- 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.
- Bootstrap facile à partager entre le serveur HTTP et la CLI
- 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.
| Couche | Que 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 intercepteur | validation, 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.
| Article | Rôle |
|---|---|
Code | Classifications telles que bad_request et not_found |
Public | Mention pouvant être restituée au client |
Internal | Erreur détaillée interne |
Meta | Journaux 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.
| Commander | Choses à faire |
|---|---|
| 1 | Recevoir le signal de terminaison sur signal.NotifyContext |
| 2 | Arrêtez les nouveaux afflux en supprimant readiness |
| 3 | Fermer la file d’attente entrée consommateur/cron/travailleur en arrière-plan |
| 4 | drain pendant un moment et attendez que l’équilibreur de charge soit reflété |
| 5 | Propager l’annulation du contexte au gestionnaire |
| 6 | Appelez server.Shutdown(timeoutCtx) |
| 7 | Attendez la fin de la tâche en vol le WaitGroup / errgroup |
| 8 | Fermez enfin DB/cache/broker producteur |
| 9 | Utilisez 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.
| Nom | Signification |
|---|---|
myapp health | CLI pour diagnostiquer directement les dépendances localement |
GET /api/health | Point 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.
hsb.horse