Décision d’architecture · Backend · Octobre 2026

Go ou Rust pour un backend ? Comment choisir quand on aime les deux

Go ou Rust ? Dès qu’un nouveau backend se prépare, le débat revient, et il tourne vite au match entre deux camps. Je l’ai vécu de l’intérieur, avec un handicap : j’aime les deux. Mon projet, une application mobile pour iOS et Android, demandait un serveur économe, sûr, rapide, et qui ménage la batterie des téléphones. Comment trancher quand les deux langages cochent presque toutes les cases ? Voici comment j’ai décidé, ce qui a fait pencher la balance, et ce que j’ai accepté de perdre.

Écrit pour
  • les devs
  • les CTO
Statut
Acceptée
Contexte
Backend d’une application mobile native
Options
Go, Rust
Critères décisifs
Le protocole, l’équipe de demain, l’exploitation
Réversible ?
Oui, service par service

J’aime Go. J’aime Rust. Je le dis d’entrée, parce que ce débat se joue d’habitude entre deux camps qui ne s’écoutent pas : ceux pour qui Rust est la seule réponse sérieuse à tout, et ceux pour qui c’est une complication dont personne n’a besoin. Je ne suis dans aucun des deux. J’ai écrit du code dans les deux, et chacun m’a appris quelque chose que l’autre ne m’aurait pas appris.

Alors, quand il a fallu choisir le langage du backend de mon projet, une application mobile native, j’ai vraiment hésité. Pas entre un bon et un mauvais langage : entre deux excellents outils qui n’ont pas fait les mêmes paris. Cet article raconte cette hésitation, et comment j’en suis sorti.

La question qu’on pose partout Quel est le meilleur langage pour un backend ?
La question que je me suis posée Pour ce projet, ces contraintes et l’équipe qui le maintiendra dans trois ans, quel langage fait le moins de compromis sur ce qui compte vraiment ?

La première question n’a pas de réponse. Ou plutôt, elle en a mille, et elles se contredisent toutes. La seconde en a une, mais elle n’est valable que pour un projet. C’est pour ça que je vais commencer par le projet, et pas par les langages.

J’ai écrit cet article comme on écrit une décision d’architecture dans une équipe : un ADR, pour Architecture Decision Record. Le format, popularisé par Michael Nygard, est d’une simplicité désarmante : le contexte, les options, la décision, ses conséquences. Il oblige à écrire pourquoi, pas seulement quoi. Et il laisse une trace pour celui qui, dans deux ans, ouvrira le dépôt en se demandant « mais pourquoi diable ont-ils pris Go ? ». Ce sera peut-être moi.

Cet article décrit les deux écosystèmes tels qu’ils sont fin octobre 2026. Les grandes lignes tiendront longtemps ; les bibliothèques, elles, bougent vite, en particulier gRPC côté Rust, qui vit une transition importante (j’y reviens au § 4). Je mettrai l’article à jour quand l’essentiel changera.
§ 1 · contexte

Le projet, et ce qu’il exige

Un langage ne se choisit pas dans l’absolu. Il se choisit contre des contraintes. Voici le projet, et les quatre contraintes qui ont fait tout le travail.

Une application mobile, et ses trois publics

Le projet est une application mobile native, pour iOS et Android. Son backend fait ce que font la plupart des backends : authentifier, stocker, synchroniser, notifier. Rien d’exotique, et c’est justement ce qui rend le choix intéressant : ni Go ni Rust n’est disqualifié d’office. Ce backend sert trois publics :

Les trois publics du backend. À gauche, l’application, sur des téléphones iOS et Android : presque tout le trafic. À droite, le panneau d’administration, ouvert par l’équipe dans un navigateur, et les services tiers, qui envoient des webhooks. Tous parlent au même backend, au centre, qu’il reste à écrire, en Go ou en Rust. presque tout le trafic L’application iOS Android Le backend le serveur à écrire en Go ou en Rust ? Panneau d’administration l’équipe, dans un navigateur Services tiers webhooks, événements
Trois publics, un seul backend. Reste à savoir dans quel langage l’écrire, et par quel protocole chacun lui parle.

L’application est de loin le plus gros consommateur, et elle parle depuis un réseau mobile qui va et vient. Gardez ce schéma en tête : il reviendra au § 2.

Quatre contraintes, qui tirent chacune de leur côté

Les coûts d’infrastructure

Chaque mégaoctet de mémoire et chaque milliseconde de CPU se paient, tous les mois, pour toujours. Je veux des instances petites, peu nombreuses, qui démarrent vite. Et surtout, peu de pièces à faire tourner : chaque composant de plus, c’est une facture, une supervision et une astreinte de plus.

Ce que ça demande au backend : une empreinte mémoire modeste, un démarrage rapide, un seul binaire, un seul port.

La sécurité

Des données personnelles, une API exposée sur Internet, des comptes à protéger. Je veux éliminer le plus possible de classes de bugs dès l’écriture, garder une liste de dépendances que je peux encore auditer, et appliquer un correctif de sécurité en une heure, pas en une semaine.

Ce que ça demande au backend : un langage sûr pour la mémoire, une petite surface de dépendances, un bon outillage d’analyse des vulnérabilités.

La batterie

La contrainte la plus sous-estimée, et la plus mal comprise. Le backend ne consomme pas la batterie du téléphone : il la fait consommer. Par la taille de ce qu’il envoie, par le nombre d’allers-retours qu’il impose, et par le temps pendant lequel il garde la radio éveillée.

Ce que ça demande au backend : des messages compacts, peu d’allers-retours, une connexion réutilisée, pousser plutôt que se faire interroger.

La performance

La latence que ressent l’utilisateur, surtout sur réseau mobile, et le débit côté serveur. Avec une nuance que l’expérience m’a apprise : sur ce genre de backend, le goulot d’étranglement est presque toujours la base de données ou le réseau, rarement le langage.

Ce que ça demande au backend : une latence stable (pas seulement une bonne moyenne), et un protocole qui ne gaspille pas les allers-retours.

La batterie se joue dans le contrat, pas dans le langage

Arrêtons-nous sur la batterie, parce que c’est là que se cache l’argument le plus contre-intuitif de l’article. Après chaque échange, la radio cellulaire, l’un des composants les plus gourmands du téléphone, ne se rendort pas tout de suite : elle reste éveillée plusieurs secondes, au cas où d’autres données arriveraient (les chercheurs parlent de « queue », tail). Dix petites requêtes espacées coûtent donc bien plus cher qu’une seule, même pour le même volume :

Schéma de l’activité de la radio d’un téléphone. En haut, une application qui interroge le serveur toutes les trente secondes : chaque requête réveille la radio, qui reste ensuite éveillée pendant sa période de queue ; elle ne se rendort presque jamais. En bas, une application qui garde un flux ouvert : la radio se réveille à l’ouverture, puis une seule fois quand le serveur envoie un lot d’événements, et dort le reste du temps. Interroger toutes les 30 s la radio ne dort presque jamais Un flux ouvert le serveur ne parle que s’il a quelque chose à dire ouverture un lot d’événements la radio dort transfert de données queue : radio encore éveillée, rien n’est transmis radio en veille
Schéma sans échelle. Ce qui coûte, ce n’est pas seulement le volume transmis, c’est le nombre de fois où l’on réveille la radio. Le phénomène de queue est décrit notamment par Huang et al. (MobiSys 2012) pour la 4G.

Relisez la liste de ce que la batterie demande au backend : messages compacts, peu d’allers-retours, connexion réutilisée, pousser plutôt qu’être interrogé. Le langage du backend n’y apparaît nulle part. Ce qui y apparaît, c’est le protocole. Voilà pourquoi j’ai choisi le protocole avant le langage, et pourquoi ce premier choix a pesé lourd dans le second.

Ce que le projet n’est pas

Dans un ADR, ce qu’on exclut compte autant que ce qu’on retient. Trois « non » ont beaucoup pesé :

Pas un moteur de calcul. Le chemin critique, ce sont surtout des entrées-sorties : base de données, cache, services tiers. Le serveur passe l’essentiel de son temps à attendre.
Pas de temps réel dur. Une latence stable, oui. Une garantie à la microseconde, non. Personne n’est en danger si une réponse arrive quelques millisecondes plus tard.
Pas une équipe d’experts qui attend. L’équipe va grandir, et les personnes qui la rejoindront ne sont pas encore recrutées. Le langage doit les accueillir.

Si l’un de ces trois « non » avait été un « oui », cet article aurait probablement une autre conclusion. Je vous montrerai lequel au § 4, curseurs à l’appui.

§ 1 · contextestatut : établi

Le langage du backend n’a presque aucun effet sur la batterie du téléphone. Le protocole, si. Il fallait donc choisir le contrat avant de choisir le langage.

§ 2 · Le contrat
§ 2 · contrat

REST, gRPC, ou les deux ?

Avant de choisir le langage, il fallait choisir comment l’application parlerait à son serveur. Sur le moment, je n’ai même pas réfléchi : j’ai pris REST, comme toujours. C’est en le confrontant aux contraintes du § 1 que j’ai commencé à douter.

Le réflexe : REST

REST, je l’ai utilisé dans presque tous mes projets, et c’est sans doute votre cas aussi. Le rappel tient en une image :

Un échange REST. Le client envoie la requête GET /users/42 : un verbe HTTP et l’adresse d’une ressource. Le serveur répond 200 OK avec du JSON, qui répète le nom de chaque champ. Le client application, navigateur, curl… Le serveur une adresse par ressource GET /users/42 la requête : un verbe HTTP et l’adresse d’une ressource 200 OK {"id": 42, "name": "Awa", "premium": true} la réponse : du JSON lisible, qui répète le nom de chaque champ
REST en une image : une adresse, un verbe, du JSON. Tout est lisible, y compris le nom de chaque champ, répété dans chaque réponse.

Simple, universel, compris par tous les outils, du navigateur à curl. Mais quand j’ai relu ce que la batterie demande au backend, aucune case n’était cochée d’office :

Rien d’insurmontable : on peut compresser, générer les clients, ajouter une WebSocket. Mais chaque fois, c’est une brique de plus, à poser soi-même, des deux côtés. Je me suis donc demandé s’il existait un protocole qui fasse tout cela par construction.

Ce que j’ai trouvé à côté

Deux noms revenaient sans cesse.

GraphQL

né chez Facebook

Une seule adresse, et c’est le client qui choisit exactement les champs qu’il veut. Un vrai atout quand des écrans très différents lisent les mêmes données. Mais les messages restent du JSON.

gRPC

né chez Google

On n’appelle plus une adresse, mais une fonction du serveur, décrite dans un contrat. Je le connaissais de réputation, pour faire dialoguer des microservices entre eux. Pas pour une application mobile.

GraphQL résolvait surtout un problème de forme des données, que je n’avais pas. gRPC, je le connaissais mal : je suis allé voir de plus près.

gRPC, en deux minutes

gRPC vient de RPC, pour Remote Procedure Call, l’appel de procédure à distance : au lieu de construire une requête HTTP, l’application appelle une fonction, et c’est le serveur qui l’exécute. Tout part d’un contrat, un fichier .proto, qui décrit au même endroit les données et les fonctions :

proto/app/sync/v1/sync.proto (extrait simplifié)
syntax = "proto3";
package app.sync.v1;

// Un seul fichier décrit tout le contrat : les données ET les appels.

// ── 1. Les messages : les données échangées, champ par champ ──
message SyncRequest {
  string device_id = 1;        // chaque champ a un type…
  int64 since_version = 2;     // …et un numéro : c’est lui, pas le nom, qui voyage
  repeated Change changes = 3; // une liste de changements
}

message SyncResponse {
  int64 version = 1;
  repeated Change changes = 2;
}
// (Change, WatchRequest et Event se déclarent de la même façon.)

// ── 2. Le service : les fonctions que l’application peut appeler ──
service SyncService {
  // Envoyer ses changements et recevoir ceux du serveur, en un seul aller-retour.
  rpc Sync(SyncRequest) returns (SyncResponse);
  // Tant que l’application est au premier plan, le serveur pousse ce qui change.
  rpc Watch(WatchRequest) returns (stream Event);
}

De ce seul fichier, des générateurs tirent le code de chaque côté :

Un contrat, le même code pour tous. Le fichier sync.proto passe dans des générateurs, qui produisent trois codes : le serveur, où il reste à écrire les fonctions, le client Swift de l’application iOS et le client Kotlin de l’application Android, prêts à appeler. Dans les trois, on retrouve les mêmes noms : SyncRequest, SyncResponse, Sync et Watch. sync.proto le contrat, écrit une fois messages et service générateurs les mêmes noms, dans chaque code généré Le serveur les fonctions à écrire SyncRequest SyncResponse Sync() Watch() Client Swift iOS, prêt à appeler SyncRequest SyncResponse sync() watch() Client Kotlin Android, prêt à appeler SyncRequest SyncResponse sync() watch()
Le contrat est écrit une fois, et chacun en reçoit sa version : le serveur, l’application iOS, l’application Android. Mêmes messages, mêmes champs, mêmes fonctions. (Noms simplifiés : chaque langage adapte la casse et les préfixes à ses conventions.)

À l’exécution, l’application appelle la fonction comme si elle était locale. Le code généré s’occupe du réseau : un format binaire compact, Protocol Buffers, sur une connexion HTTP/2 ouverte une fois.

Un appel gRPC. L’application appelle client.sync comme une fonction ordinaire ; la requête part en protobuf binaire sur une connexion HTTP/2 ouverte une fois et réutilisée ; le serveur exécute la fonction Sync et renvoie la réponse. L’application client.sync(request) SyncRequest · protobuf binaire une connexion HTTP/2, ouverte une fois, réutilisée SyncResponse Le serveur func Sync(ctx, req)
L’application appelle sync comme une fonction ordinaire. Le réseau est caché derrière le code généré.

Ce qui m’a convaincu

Ce n’est pas la vitesse qu’on prête à gRPC qui m’a arrêté. C’est le contrat. Un champ renommé casse la compilation, pas la production. Et les règles de compatibilité de Protocol Buffers (on ajoute des champs, on ne réutilise jamais un numéro) laissent évoluer l’API sans casser les anciennes versions de l’application, qui resteront installées des mois sur certains téléphones.

Restait à le confronter aux contraintes du § 1. Premier test, la taille des messages :

Le même message en JSON et en protobuf. En JSON, {"id":42,"name":"Awa","premium":true} fait 37 octets, dont les noms des champs, répétés dans chaque message. En protobuf, il fait 9 octets : chaque nom est remplacé par un numéro de champ, sur un octet. JSON 37 octets { " i d " : 4 2 , " n a m e " : " A w a " , " p r e m i u m " : t r u e } Protobuf 9 octets 08 2A 12 03 41 77 61 18 01 les noms des champs, répétés dans chaque message le numéro du champ (1, 2, 3), sur un octet, remplace son nom
Le même message, sans compression. L’écart réel dépend des messages, mais le principe reste : protobuf n’envoie que des numéros et des valeurs.

Le reste tient dans un tableau. Sur presque chaque ligne, gRPC fait par construction ce que REST demande d’ajouter :

Pour l’application mobileRESTGraphQLgRPC
Des messages compacts
Batterie
JSON, texteJSON, seulement les champs demandésBinaire compact
Une connexion pour tous les appels
BatteriePerformance
Possible, en HTTP/2Possible, en HTTP/2Toujours : HTTP/2 est son transport
Le serveur qui pousse
Batterie
À ajouter (SSE, WebSocket)Abonnements, souvent sur WebSocketFlux intégrés au protocole
Un contrat qui génère le code des clients
Sécurité
En option (OpenAPI)Oui (schéma typé)Oui, c’est son cœur
Une échéance et une annulation qui voyagent avec l’appel
Coûts
À construire soi-mêmeÀ construire soi-mêmeIntégrées au protocole
Dans un navigateurNatifNatifVia gRPC-Web ou Connect
Lire et tester à la maincurl, partoutBons outils dédiésOutils dédiés (grpcurl)

GraphQL gardait un atout, la liberté du client. Mais mon problème, c’était la batterie, et sur ce terrain, gRPC partait avec plusieurs longueurs d’avance.

Ce que gRPC coûte

Je me méfie des articles qui présentent gRPC comme une évidence. J’ai donc regardé l’envers du décor :

On ne lit plus ses requêtesLe binaire ne se lit pas à l’œil nu, et curl ne suffit plus. Il faut grpcurl, la réflexion activée sur le serveur, ou un client graphique qui comprend gRPC.
Pas de cache HTTPLes appels gRPC passent en POST : les caches et les CDN ne servent plus à rien. Ce qui doit être mis en cache publiquement n’a rien à faire en gRPC.
La répartition de charge se compliqueLes connexions HTTP/2 durent longtemps. Un répartiteur qui travaille au niveau des connexions colle un client à une instance. Il faut répartir appel par appel, ou côté client.

Le vrai problème : les navigateurs

Restait le panneau d’administration. Il vit dans un navigateur, et un navigateur ne donne pas au JavaScript le contrôle d’HTTP/2 dont gRPC a besoin : il faut passer par gRPC-Web ou par le protocole Connect, avec un proxy ou un serveur compatible. Faisable, mais je voulais le bon outil à chaque endroit, pas un outil qu’on tord pour qu’il aille partout.

Or, dans un navigateur, le bon outil, c’est REST : du JSON lisible, curl pour tester, OpenAPI pour documenter. Même constat pour les services tiers, qui envoient leurs webhooks en JSON et ne parleront jamais notre protobuf.

La question qu’on se pose d’abord gRPC ou REST ?
La bonne question Et si l’on gardait les deux, chacun là où il est le meilleur ?

gRPC d’abord, REST là où il le faut

C’est la réponse que j’ai retenue. Reprenons le schéma du § 1, avec les deux protocoles :

Le même schéma, avec les deux protocoles. À gauche, dans le cercle gRPC, l’application iOS et Android, qui appelle Sync et Watch : presque tout le trafic, plus quelques appels REST. À droite, dans le cercle REST, le panneau d’administration et les services tiers, qui échangent du JSON. Au centre, un seul backend : un seul port, un seul binaire, la même logique métier. gRPC REST Sync · Watch quelques appels REST JSON JSON L’application iOS Android Le backend un seul port, un seul binaire la même logique métier Panneau d’administration l’équipe, dans un navigateur Services tiers webhooks, événements
Deux protocoles, un seul backend. Les téléphones passent aussi par REST, pour quelques besoins précis.

L’application parle gRPC : c’est presque tout le trafic. REST garde ce qui parle HTTP par nature : le navigateur, les webhooks, et quelques besoins des téléphones eux-mêmes, comme les fichiers des liens universels ou le téléversement de gros fichiers.

gRPC, pour l’application

Côté application, le contrat tient dans les deux appels du .proto : Sync regroupe tout en un aller-retour, Watch laisse le serveur pousser. Leur rythme suit celui de l’application :

  1. Au premier planWatch est ouvert

    Le serveur pousse ce qui change. Fini le « y a-t-il du nouveau ? » toutes les trente secondes.

  2. En arrière-planLe système suspend l’application

    Le flux finit par se fermer. Si quelque chose d’important arrive, une notification push (APNs, FCM) prend le relais.

  3. Au retourSync, puis Watch

    Un seul aller-retour rattrape ce qui a été manqué, et le flux reprend.

Le piège du keepalive. Faire envoyer un ping toutes les dix secondes pour garder la connexion ouverte, c’est réveiller la radio à chaque fois : exactement ce qu’on voulait éviter. grpc-go coupe d’ailleurs les clients qui pinguent plus souvent que sa politique ne l’autorise (cinq minutes par défaut). Sur mobile, mieux vaut laisser la connexion mourir en arrière-plan, et la rouvrir au retour.

Pourquoi ce mélange pèse sur le choix du langage

C’est ici que le protocole rattrape le langage :

§ 2 · contratstatut : gRPC principal, REST secondaire, un seul port

Le contrat est posé. Il transforme une question de goût (« quel langage je préfère ? ») en une question d’écosystème (« lequel sert le mieux ce contrat ? »). Place aux deux candidats.

§ 3 · Les options
§ 3 · options

Go et Rust, vus de l’atelier

Je ne vais pas refaire le comparatif qu’on trouve partout, avec ses tableaux de benchmarks et ses listes de fonctionnalités. Voici ce que je regarde, moi, quand je compare deux langages pour un service qui doit vivre longtemps, et pourquoi.

Ce que je ne compare pas

Les benchmarks « hello world »

Ils mesurent un framework qui renvoie une chaîne de caractères. Mon service, lui, attend une base de données. Le gagnant d’un benchmark de ce genre ne dit rien du mien.

La syntaxe

On s’habitue à n’importe quelle syntaxe en quelques semaines. Ce qui reste, ce sont les idées que le langage vous impose, ou vous épargne.

La popularité sur les réseaux

L’enthousiasme d’une communauté est un excellent signal pour apprendre un langage. C’est un mauvais signal pour choisir celui d’un produit.

Ce que fait la grande entreprise d’à côté

Elle n’a ni mon équipe, ni mon budget, ni mes contraintes. Ses raisons ne sont pas les miennes.

Ce que je compare

Le modèle de concurrence

Un backend passe son temps à attendre des milliers de choses à la fois.

Comment le langage l’exprime-t-il, et à quel prix pour celui qui lit le code ?

La mémoire et la latence

Ramasse-miettes ou pas ? Ce n’est pas une question de vitesse moyenne.

La latence est-elle régulière, et quelle taille d’instance faudra-t-il payer ?

Les erreurs et les absences

C’est là que naissent la plupart des incidents de production.

Comment le langage traite-t-il ce qui échoue, et ce qui n’existe pas ?

La boucle de retour

Compilation, tests, redémarrage : elle conditionne tout le reste.

Combien de temps entre « je modifie » et « je sais si ça marche » ?

Le coût de la lecture

On lit le code bien plus souvent qu’on ne l’écrit, et de plus en plus, on relit celui qu’une IA a écrit.

Un nouveau venu peut-il relire ce code, et juger s’il est juste ?

La vie en production

Construire, déployer, profiler, mettre à jour.

Le code d’aujourd’hui compilera-t-il encore dans cinq ans ?

Option A

Go, le pari de l’ennui

Un langage qui a fait le choix délibéré d’en faire moins, pour que des équipes entières puissent en faire plus.

Go a été conçu chez Google avec un objectif que Rob Pike a très bien résumé : servir le génie logiciel à grande échelle, avec des équipes nombreuses, des bases de code énormes et des développeurs qui changent. D’où ses choix, souvent moqués : peu de mots-clés, une seule façon de formater le code (gofmt), pas d’héritage, pas de magie. Le résultat est un langage ennuyeux, au meilleur sens du terme : le code Go d’un projet ressemble au code Go d’un autre projet. On ouvre un dépôt inconnu, et on le lit.

Pour la concurrence, Go a les goroutines : des fils d’exécution très légers, gérés par le runtime, qu’on lance par milliers sans y penser. Le code reste séquentiel à lire, le runtime s’occupe de l’ordonnancement. Pour un backend qui attend la base, le réseau et des services tiers, c’est exactement le bon modèle. Pour la mémoire, un ramasse-miettes concurrent, conçu pour des pauses courtes. Il continue de progresser : depuis Go 1.26, un nouveau collecteur (Green Tea) est activé par défaut, et les notes de version annoncent une réduction notable du temps passé à collecter pour les programmes qui allouent beaucoup.

Ce que j’aime

  • Une compilation quasi instantanée, et un binaire statique unique à déployer.
  • Une bibliothèque standard immense et solide : serveur HTTP, TLS, cryptographie, tests, profilage (pprof).
  • La promesse de compatibilité de Go 1 : le code écrit il y a dix ans compile toujours.
  • Un outillage livré avec le langage : formatage, tests, détecteur de data races, fuzzing, analyse de vulnérabilités.
  • Un code que n’importe quel développeur backend relit dès sa première semaine.

Ce qui me coûte

  • nil, et son cousin pervers : l’interface qui contient un pointeur nul et qui n’est pourtant pas nil.
  • Le if err != nil répété à chaque ligne. Explicite, oui. Verbeux, aussi.
  • Un système de types modeste : des génériques depuis Go 1.18, mais pas de vrais types somme, pas d’énumérations dignes de ce nom.
  • Les data races restent possibles : on les détecte en testant, le compilateur ne les interdit pas.
Option B

Rust, le compilateur qui relit avec vous

Un langage qui a fait le pari inverse : tout dire au compilateur, pour qu’il puisse tout vérifier.

Rust n’a pas de ramasse-miettes. La mémoire est gérée par un système de propriété et d’emprunts que le compilateur vérifie : chaque valeur a un propriétaire, on peut la prêter en lecture à plusieurs ou en écriture à un seul, jamais les deux à la fois. Cette règle, qui fait tant souffrir au début, a une conséquence remarquable : les accès concurrents non synchronisés à la mémoire (les fameuses data races) deviennent des erreurs de compilation.

Ce qui me séduit le plus, pourtant, ce n’est pas la performance. C’est la modélisation. Des énumérations qui portent des données, un match qui refuse de compiler si l’on oublie un cas, Option à la place de nil, Result et l’opérateur ? pour les erreurs. On décrit l’état d’une commande, d’un paiement ou d’une session avec une précision que Go ne permet pas, et le compilateur garde cette précision pour vous. Quand un programme Rust compile, toute une catégorie de questions disparaît de la revue de code.

Et la performance est bien là. Sans ramasse-miettes, la latence est régulière et l’empreinte mémoire faible. L’histoire la plus citée est celle de Discord, qui a réécrit en Rust, en 2020, un service Go dont le ramasse-miettes provoquait des pics de latence réguliers. Le ramasse-miettes de Go a beaucoup progressé depuis, mais le principe tient : ce qu’on ne collecte pas ne coûte rien. Cloudflare a de son côté remplacé une partie de son infrastructure de proxy par Pingora, écrit en Rust.

Ce que j’aime

  • Pas de null, pas de data race, pas d’erreur ignorée par inadvertance : c’est le compilateur qui y veille.
  • Des types qui décrivent le métier, et des cas impossibles qui deviennent impossibles à écrire.
  • Une latence régulière et une empreinte mémoire très faible, donc des instances plus petites.
  • cargo, qui reste à mes yeux l’un des meilleurs gestionnaires de paquets qui existent.
  • Ce sentiment rare : « si ça compile, ça marche », presque toujours vrai.

Ce qui me coûte

  • La courbe d’apprentissage : les emprunts et les durées de vie demandent un vrai investissement.
  • L’asynchrone, la partie la plus difficile du langage : choix du runtime, contraintes Send, Pin, messages d’erreur intimidants.
  • Des temps de compilation longs, qui allongent la boucle de retour.
  • Un arbre de dépendances large : même le runtime asynchrone est une bibliothèque externe.

Le même code, deux philosophies

Rien ne vaut un exemple. Voici la même fonction métier dans les deux langages : récupérer le profil d’un utilisateur, en refusant les comptes supprimés.

Go
func (s *Service) Profile(ctx context.Context, id string) (*Profile, error) {
	u, err := s.users.Get(ctx, id)
	if err != nil {
		return nil, fmt.Errorf("profil %s : %w", id, err)
	}
	if u.Status == StatusDeleted {
		return nil, ErrNotFound
	}
	// Un nouveau statut ajouté demain ?
	// Rien ne m’obligera à revenir ici.
	return toProfile(u), nil
}
Rust
async fn profile(&self, id: UserId) -> Result<Profile, AppError> {
    let user = self.users.get(id).await?; // l’erreur remonte
    match user.status {
        Status::Active => Ok(Profile::from(user)),
        Status::Deleted => Err(AppError::NotFound),
        // Un nouveau statut ajouté demain ?
        // Le code ne compile plus tant
        // qu’on ne l’a pas traité ici.
    }
}

Le code Go est plus long, et tout est sous vos yeux : chaque erreur, chaque retour. Un développeur qui n’a jamais écrit de Go le comprend en le lisant. Le code Rust est plus dense et plus sûr : l’erreur est typée, l’oubli d’un cas est impossible. Mais une partie du travail est ailleurs (la conversion des erreurs vers AppError, définie une fois pour toutes), et il faut connaître le langage pour la voir.

Aucun des deux n’est « meilleur ». Go optimise pour la personne qui lit le code sans le connaître. Rust optimise pour la personne qui le modifie dans six mois sans tout se rappeler. Les deux ont raison ; la question est de savoir laquelle de ces deux personnes votre projet a le plus besoin de protéger. Et pour moi, un critère récent a fait pencher la réponse.

Un critère de plus

Le code qu’on n’a pas écrit soi-même

Aujourd’hui, et plus encore demain, une bonne part du code sera écrite par une IA. Le développeur écrit moins et relit davantage : c’est la thèse de mon premier article, « L’IA va-t-elle remplacer les développeurs ? Mauvaise question. ». Dans ce monde-là, un code facile à lire n’est plus un confort : c’est un critère de choix, et il a pesé dans le mien.

Pour qui connaît bien Rust, la question ne se pose presque pas : il relit le code généré sans peine, et le compilateur rattrape au passage bien des erreurs de l’IA. Mais pensez au développeur qui rejoint l’équipe et qui code avec un assistant. En Go, ce que l’IA produit se lit comme le reste du dépôt, et il peut juger si c’est juste. En Rust, les durées de vie, les traits et l’asynchrone demandent de bien connaître le langage pour juger un code qu’on n’a pas écrit. Accepter un code qu’on ne comprend pas, simplement parce qu’il compile, c’est exactement ce que je veux éviter.

§ 3 · optionsstatut : deux candidats sérieux

Sur le papier, j’aurais pu défendre l’un ou l’autre devant n’importe quel comité d’architecture. Il fallait donc sortir du papier, et confronter chaque écosystème aux contraintes réelles du projet.

§ 4 · Les critères
§ 4 · critères

Les écosystèmes face à mes contraintes

On n’écrit jamais un backend avec un langage seul. On l’écrit avec un compilateur, des bibliothèques, des outils, des habitudes et des gens. C’est cet ensemble que j’ai comparé, critère par critère, avec une question simple à chaque fois : lequel sert le mieux ce projet ?

gRPC, le critère numéro un

Avantage Go
Gogrpc-go est l’une des implémentations officielles de gRPC depuis le début, maintenue par le projet gRPC lui-même. Tout y est : intercepteurs, service de santé standard, réflexion, keepalive, répartition de charge côté client, xDS. Autour, un écosystème complet : go-grpc-middleware (authentification, journaux, récupération des paniques, limitation de débit), l’instrumentation OpenTelemetry, grpc-gateway pour générer du REST depuis le contrat, ConnectRPC, et la validation protovalidate.
Rusttonic, bâti sur hyper, tower et tokio, avec prost pour Protocol Buffers, est une très bonne bibliothèque, éprouvée en production, avec ses modules de santé, de réflexion et de gRPC-Web. Mais l’écosystème est en pleine transition :
  1. Jusqu’icitonic

    La bibliothèque de référence, portée par la communauté.

  2. Mai 2026tonic rejoint le projet gRPC

    Et passe en maintenance : corrections uniquement.

  3. À venirUne crate officielle grpc

    Avec la gestion des connexions et la répartition de charge côté client.

Une excellente nouvelle pour Rust, à moyen terme. Mais pour un projet qui démarre, c’est une transition à traverser : la bibliothèque choisie aujourd’hui n’est pas celle qu’on utilisera dans deux ans. Côté Go, la question ne se pose pas.

REST et HTTP

Égalité
Gonet/http est l’un des meilleurs serveurs HTTP de toutes les bibliothèques standards, et depuis Go 1.22 son routeur gère les méthodes et les paramètres de chemin. Au-dessus, Chi, Echo ou Gin. J’évite les frameworks qui quittent net/http, comme Fiber, qui repose sur un autre moteur HTTP.
Rustaxum, développé par l’équipe de tokio, est remarquable : typé, composable, sans macros envahissantes. actix-web est très rapide et très mûr. Rien à redire.

Comme prévu au § 2 : un critère qu’on vérifie, pas un critère qui décide.

Un seul port pour gRPC et REST

Égalité
GoPlusieurs solutions éprouvées : aiguiller les connexions avec cmux, faire passer gRPC par le serveur HTTP standard (ServeHTTP, encore expérimental), ou adopter ConnectRPC, qui sert gRPC, gRPC-Web et Connect avec de simples handlers net/http. J’y reviens en détail au § 6.
Rusttonic et axum reposent sur les mêmes fondations (hyper et tower). Les composer sur le même port est presque naturel : ce ne sont que deux services tower à router.

Je le reconnais volontiers : sur ce point précis, la solution Rust est même plus élégante. Elle n’est pas plus simple à exploiter pour autant.

Sécurité

Match nul, pour des raisons opposées
GoSûr pour la mémoire (hors unsafe et data races). La cryptographie et TLS sont dans la bibliothèque standard, maintenus par l’équipe Go, avec un module certifiable FIPS 140-3 depuis Go 1.24. govulncheck ne signale que les vulnérabilités du code réellement appelé, ce qui évite de noyer les alertes utiles. Et peu de dépendances, donc peu de choses à auditer.
RustDes garanties plus fortes, dès la compilation : ni data race, ni pointeur nul. rustls pour TLS, cargo audit (base RustSec), cargo deny et cargo vet pour la chaîne d’approvisionnement. Mais un service tonic tire un arbre de dépendances nettement plus large, et chacune est une porte d’entrée potentielle.

Rust gagne sur les garanties du langage, Go sur la surface d’attaque et la simplicité de l’outillage. Pour un backend d’API, où les vulnérabilités viennent bien plus souvent d’une erreur de logique (une autorisation oubliée) que de la mémoire, je considère que c’est un match nul.

Coûts et performance

Avantage Rust
GoRapide, avec une empreinte mémoire raisonnable. GOMEMLIMIT permet de dire au ramasse-miettes combien de mémoire le conteneur peut utiliser, et le nouveau collecteur réduit encore son coût. Démarrage quasi instantané.
RustPlus économe, sans discussion : moins de mémoire, pas de collecte, plus de requêtes par cœur. Pour un service qui calcule, l’écart peut être important.

L’écart est réel, mais il fond quand le service passe son temps à attendre une base de données. Et dans un projet qui démarre, la ressource la plus chère n’est pas le CPU : c’est le temps des développeurs. Rust gagne sur la facture d’infrastructure ; sur le coût total, c’est beaucoup moins net.

Exploitation

Avantage Go
GoCompilation croisée triviale (GOOS, GOARCH), binaire statique, image de conteneur minuscule. Le profilage est intégré au langage : on branche pprof et l’on voit où part le CPU et la mémoire, en production. Et les outils du cloud (Kubernetes, Docker, Prometheus, etcd…) sont écrits en Go : leurs bibliothèques clientes sont les plus complètes.
RustBinaire statique et image minuscule aussi. La compilation croisée demande plus de préparation (cibles, éditeur de liens, ou des outils comme cross). Le profilage passe par les outils du système, très bons mais moins intégrés.

Les deux s’exploitent bien. Go demande simplement moins de connaissances pour le faire.

Recruter, intégrer, transmettre

Avantage Go
GoDans le sondage Stack Overflow 2025, 16,4 % des répondants disent utiliser Go. Surtout, Go s’apprend vite : sa spécification est courte, et un développeur backend venu de Java, de PHP ou de Node.js peut contribuer à une base de code Go très tôt, parce qu’il n’y a presque rien à deviner.
Rust14,8 % des répondants utilisent Rust, et il est depuis des années le langage le plus « admiré » du même sondage (72 % en 2025). Chez Google, plus des deux tiers des développeurs passés à Rust se sentaient capables de contribuer en deux mois ou moins ; mais seulement un tiers environ étaient aussi productifs qu’avec leur langage précédent au bout de deux mois, et un peu plus de la moitié au bout de quatre.

L’écart d’usage est plus faible qu’on ne le dit, et la légende du « six mois pour apprendre Rust » est exagérée. Mais mon problème n’est pas de recruter un expert : c’est de rendre n’importe quel bon développeur backend productif sur la base de code, vite, et sans qu’il ait besoin d’un mentor Rust à côté de lui. Sur ce point, Go gagne nettement.

Le résumé, sur une page

CritèreGoRustPour ce projet
Écosystème gRPCOfficiel, complet, stableTrès bon, en transition vers l’officielGo
RESTnet/http, Chiaxum, actix-webÉgalité
Un seul portcmux, ServeHTTP, Connecttonic + axum sur towerÉgalité
SécuritéPetite surface, bon outillageGaranties du compilateurÉgalité
Mémoire, latenceRamasse-miettes, bien régléPas de collecte, très économeRust
Boucle de retourCompilation quasi instantanéeCompilation longueGo
ExploitationSimple, profilage intégréBonne, plus exigeanteGo
Équipe de demainVite productivePlus longue à formerGo
Modélisation du métierTypes modestesÉnumérations, Option, ResultRust

Vu comme ça, Go semble gagner largement. C’est trompeur : un tableau compte les lignes, il ne les pèse pas. Or tout le débat est là. Si la latence et la modélisation comptent deux fois plus que le reste pour vous, le tableau ne veut plus rien dire. D’où l’outil ci-dessous.

La pesée

Mes notes (sur 5) pour chaque langage, pour ce type de backend. Elles sont subjectives, et je les assume. Ce qui compte, c’est le mécanisme : changez le poids de chaque critère selon votre projet, et regardez le gagnant changer.

Go—
Rust—

Score = somme des notes pondérées, ramenée à 100. Les préréglages ne changent que les poids, pas les notes ; dans la réalité, une équipe experte en Rust changerait aussi les notes de recrutement et d’itération.

Avec mes poids, Go l’emporte d’une demi-douzaine de points : une vraie avance, pas un écrasement. Cochez maintenant la case : un seul critère qui bouge d’un point, et l’écart fond de moitié. Voilà, en une case à cocher, pourquoi la décision n’a pas été facile. Voilà aussi pourquoi je la réexaminerai.

§ 4 · critèresstatut : pesés

La balance penche, mais pas au point de rendre le choix évident. Il fallait décider, et assumer ce que la décision coûte.

§ 5 · La décision
§ 5 · décision

Go, sans regret, mais pas sans hésiter

Décision

Le backend est écrit en Go. gRPC pour l’application, REST pour le panneau d’administration et quelques endpoints mobiles, le tout servi par un seul binaire, sur un seul port.

Prise en connaissance de cause, avec ce qu’elle coûte. Réexaminée si l’une des conditions décrites plus bas change.

Pourquoi Go

  1. Le meilleur support de gRPC, aujourd’hui

    C’est la raison qui a fait basculer la balance. gRPC est le cœur du backend, et grpc-go est officiel, complet et stable depuis des années, avec tout l’écosystème qui va autour. Côté Rust, la qualité est là, mais la bibliothèque de référence change de mains. Je ne voulais pas bâtir le cœur du projet sur une transition.

  2. Recruter, intégrer, transmettre

    L’équipe va grandir. Avec Go, un bon développeur backend, quel que soit son langage d’origine, sera utile très vite, et la base de code restera lisible par tous, y compris ce que son assistant d’IA y ajoutera. Un projet ne vit pas seulement avec ceux qui l’ont commencé : il vit avec ceux qui le reprennent.

  3. Une boucle de retour courte

    Compiler, tester, relancer : en Go, c’est quasi instantané. Au début d’un produit, on change d’avis tous les jours, et la vitesse à laquelle on peut se tromper puis corriger compte plus que la vitesse du binaire.

  4. Une surface d’attaque réduite

    TLS, cryptographie, serveur HTTP, tests : la bibliothèque standard couvre l’essentiel. Moins de dépendances, c’est moins de choses à auditer, à mettre à jour, et moins de portes pour une attaque par la chaîne d’approvisionnement. govulncheck fait le reste.

  5. Une exploitation sans surprise

    Un binaire statique, une image minuscule, la compilation croisée en une variable d’environnement, le profilage intégré, et une promesse de compatibilité qui me garantit que la montée de version ne sera pas un chantier. Pour une petite équipe, c’est de l’astreinte en moins.

  6. Assez rapide, et de mieux en mieux

    Le service attend surtout la base de données. Pour ce profil, Go est largement assez rapide, et son ramasse-miettes s’améliore à chaque version. « Assez rapide » n’est pas une excuse : c’est un critère, à condition de l’avoir vérifié sur sa propre charge plutôt que sur un benchmark.

Ce que je sacrifie, et comment je compense

Je ne vais pas faire semblant : en choisissant Go, je renonce à des choses que j’aime vraiment dans Rust. Mais un renoncement assumé se compense. Voici, pour chacun, ce que j’ai mis en place.

Je renonce à : un compilateur qui interdit les data racesEn Rust, un accès concurrent non protégé ne compile pas.
Je compense par : le détecteur de Go, partoutgo test -race dans l’intégration continue, sur toute la suite de tests, et des tests de charge qui l’activent aussi.
Je renonce à : Option, Result et de vraies énumérationsModéliser un état exhaustif, et laisser le compilateur vérifier qu’aucun cas n’est oublié.
Je compense par : des linters et des conventionsstaticcheck et golangci-lint, l’analyse nilaway d’Uber contre les pointeurs nuls, et des machines à états écrites explicitement, avec un test par transition.
Je renonce à : l’absence de ramasse-miettesUne latence parfaitement régulière et une mémoire au plus juste.
Je compense par : mesurer avant d’optimiserGOMEMLIMIT réglé sur la taille du conteneur, pprof en production, et des allocations surveillées sur les chemins les plus fréquentés.
Je renonce à : « si ça compile, ça marche »Ce confort rare que donne Rust.
Je compense par : des tests, encore des testsTests de table, fuzzing intégré (go test -fuzz) sur tout ce qui décode des entrées extérieures, et tests de contrat sur les messages protobuf.

Ce qui me ferait changer d’avis

Une décision qu’on ne peut pas remettre en cause n’est pas une décision, c’est un dogme. Voici les trois événements qui me feraient rouvrir cet ADR :

Compris après coup

Le meilleur argument pour gRPC : il rend le choix du langage réversible, service par service.

Le contrat est dans les fichiers .proto, pas dans le code Go. Demain, un service Rust peut rejoindre le backend derrière le même contrat, et l’application ne verra pas la différence. Choisir gRPC, c’est rendre le choix de Go beaucoup moins définitif.

L’applicationle contrat .protoun service Go, aujourd’huiun service Rust, demain ?

Il n’y a pas de meilleur langage

Je le dis sans détour, parce que c’est le cœur de cet article : il n’existe pas de meilleur langage. Il existe des langages qui font des paris différents. Go parie que la simplicité passe à l’échelle des équipes. Rust parie que la rigueur passe à l’échelle des systèmes. Les deux ont raison, et l’histoire le leur a donné. La bonne question n’est jamais « lequel est le meilleur ? », mais « lequel de ces deux paris mon projet a-t-il besoin de gagner ? ».

Si j’avais construit un proxy réseau, une base de données, un moteur de jeu, un logiciel embarqué ou un module WebAssembly, j’aurais choisi Rust, sans même ouvrir d’ADR. Et, petite ironie : si un jour je veux partager de la logique entre les applications iOS et Android, dans le téléphone, c’est encore vers Rust que je me tournerai, avec des outils comme UniFFI de Mozilla, qui génèrent les liaisons Swift et Kotlin.

Si votre projet est…Je pencherais pourParce que
Une API backend, des microservices, une équipe qui granditGoLisibilité, vitesse d’intégration, écosystème cloud et gRPC.
Un MVP à livrer vite, avec une équipe généralisteGoLa boucle de retour et la courbe d’apprentissage priment.
Un outil en ligne de commande, un outil d’infrastructureLes deuxDeux excellents choix ; c’est l’équipe qui décide.
Un proxy, un moteur, un service à latence critiqueRustPas de collecte, une latence régulière, chaque cycle compte.
De l’embarqué, du système, du WebAssemblyRustPas de runtime lourd, contrôle fin de la mémoire.
Une bibliothèque partagée entre iOS et AndroidRustUn cœur unique, des liaisons générées pour Swift et Kotlin.

§ 5 · décisionstatut : acceptée

La décision est prise. Reste le plus concret : comment on assemble gRPC, REST et Chi derrière un seul port, et les pièges à connaître.

§ 6 · Mise en œuvre
§ 6 · mise en œuvre

La stack retenue

Une décision d’architecture ne vaut que par sa mise en œuvre. Voici comment s’assemblent les pièces, avec le code qui compte, et les pièges à connaître avant de s’y lancer.

Go grpc-go Protocol Buffers + Buf Chi cmux OpenTelemetry Image distroless

Vue d’ensemble : un port, deux serveurs, une logique

Le chemin d’une connexion. Elle arrive sur le port 8080. cmux lit ses premiers octets : si c’est de l’HTTP/2 avec l’en-tête content-type application/grpc, elle part vers le serveur gRPC et ses intercepteurs ; sinon, vers le serveur HTTP standard et le routeur Chi avec ses middlewares. Les deux appellent la même couche de services métier, qui accède aux données. :8080 connexion entrante cmux lit le début HTTP/2 + application/grpc grpc.Server intercepteurs : auth, traces, logs tout le reste http.Server + Chi middlewares : auth, débit, CORS internal/service la logique métier, sans gRPC ni HTTP données
Chaque serveur croit avoir son propre port : c’est cmux qui leur distribue les connexions. La logique métier ne sait même pas par quelle porte la demande est entrée.

Le contrat d’abord

Tout part des fichiers .proto, rangés dans un dossier à part et versionnés avec le reste. J’utilise Buf plutôt que protoc directement : une configuration lisible pour générer le code Go (et celui des applications), un linter qui impose des conventions de nommage, et surtout une détection des changements incompatibles. C’est elle qui protège les anciennes versions de l’application installées sur les téléphones.

Dans l’intégration continue, à chaque pull request
buf lint                                   # conventions du contrat
buf breaking --against '.git#branch=main'    # aucun changement incompatible avec main
buf generate                               # code Go régénéré ; le diff doit être vide

Chi, pour le REST

Pour le REST, j’ai pris Chi, parce qu’il reste 100 % net/http : ses handlers et ses middlewares sont ceux de la bibliothèque standard, et il n’ajoute que ce qui manque (groupes de routes, sous-routeurs, middlewares éprouvés), sans aucune dépendance. Pourquoi lui plutôt que Gin, Echo ou Fiber ? La comparaison mérite un article à elle seule, et je lui en consacrerai un.

internal/rest/router.go (extrait)
func NewRouter(svc *service.Service, auth *Auth) http.Handler {
	r := chi.NewRouter()
	r.Use(middleware.RequestID, middleware.Recoverer, middleware.Timeout(15*time.Second))

	r.Get("/healthz", healthz)                                         // le répartiteur de charge
	r.With(verifySignature).Post("/webhooks/{provider}", webhook(svc))   // les services tiers
	r.With(auth.Mobile).Post("/mobile/v1/uploads", createUpload(svc)) // une URL pré-signée

	r.Route("/admin/api", func(r chi.Router) {                        // le panneau d’administration
		r.Use(auth.Admin, httprate.LimitByIP(120, time.Minute))
		r.Get("/users", listUsers(svc))
		r.Post("/users/{id}/suspend", suspendUser(svc))
	})
	return r
}

Un seul port : cmux

Reste la pièce qui fait tenir l’ensemble, celle du losange dans le schéma plus haut. cmux lit le début de chaque connexion sans le consommer, et la remet intacte au bon serveur : gRPC si elle porte content-type: application/grpc, HTTP sinon. Je l’ai choisi pour une raison précise : il laisse grpc-go utiliser son propre transport HTTP/2, optimisé pour gRPC, là où les solutions qui passent par le serveur HTTP standard en changent.

cmd/server/main.go (imports omis)
func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	svc := service.New(/* accès aux données, configuration… */)
	auth := rest.NewAuth(/* clés, sessions… */)

	lis, err := net.Listen("tcp", ":8080")
	if err != nil {
		slog.Error("écoute impossible", "err", err)
		os.Exit(1)
	}

	// Un seul port, deux serveurs : cmux lit le début de chaque connexion et l’aiguille.
	m := cmux.New(lis)
	grpcL := m.MatchWithWriters(
		cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc"),
	)
	httpL := m.Match(cmux.Any()) // tout le reste : REST

	grpcSrv := grpc.NewServer(
		grpc.ChainUnaryInterceptor(grpcapi.Recover, grpcapi.Auth, grpcapi.Log),
		grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{MinTime: 5 * time.Minute}),
	)
	grpcapi.Register(grpcSrv, svc)
	healthpb.RegisterHealthServer(grpcSrv, health.NewServer())

	var protos http.Protocols
	protos.SetHTTP1(true)
	protos.SetUnencryptedHTTP2(true) // Go 1.24+ : HTTP/2 sans TLS, derrière le répartiteur
	httpSrv := &http.Server{
		Handler:           rest.NewRouter(svc, auth),
		Protocols:         &protos,
		ReadHeaderTimeout: 5 * time.Second,
	}

	g, ctx := errgroup.WithContext(ctx)
	g.Go(func() error { return grpcSrv.Serve(grpcL) })
	g.Go(func() error { return ignoreClosed(httpSrv.Serve(httpL)) })
	g.Go(func() error { return ignoreClosed(m.Serve()) })
	g.Go(func() error {
		<-ctx.Done() // signal d’arrêt reçu, ou l’un des serveurs est tombé
		grpcSrv.GracefulStop()
		sctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		err := httpSrv.Shutdown(sctx)
		m.Close()
		return err
	})
	if err := g.Wait(); err != nil {
		slog.Error("arrêt sur erreur", "err", err)
		os.Exit(1)
	}
}

// Un serveur qu’on arrête volontairement n’est pas une erreur.
func ignoreClosed(err error) error {
	if errors.Is(err, http.ErrServerClosed) || errors.Is(err, net.ErrClosed) || errors.Is(err, cmux.ErrListenerClosed) {
		return nil
	}
	return err
}

Les pièges à connaître

Le tri se fait par connexion

cmux décide une fois, à l’ouverture. Si un client envoie du REST et du gRPC sur la même connexion HTTP/2, la deuxième famille de requêtes arrivera au mauvais serveur. En pratique, la bibliothèque gRPC et la pile HTTP de l’application ouvrent chacune leurs connexions ; mais c’est à vérifier, pas à supposer.

Les clients qui attendent le serveur

Certains clients gRPC (celui de Java, notamment) attendent que le serveur parle le premier en HTTP/2. D’où MatchWithWriters et HTTP2MatchHeaderFieldSendSettings plutôt que la règle simple : cmux envoie lui-même ce premier message.

TLS se termine en amont

Derrière cmux, http.Request.TLS reste vide, même si la connexion était chiffrée. Je termine TLS au répartiteur de charge, et le backend ne voit que du trafic interne. Si TLS doit aller jusqu’au binaire, on le termine avant cmux, et l’on ne se fie plus à ce champ.

L’arrêt propre et les flux longs

GracefulStop attend la fin de tous les appels en cours, y compris les flux du Watch, qui peuvent durer des heures. Il faut une limite : au-delà de quelques secondes, on appelle Stop, et l’application se reconnecte toute seule.

Deux sondes de santé

Le répartiteur interroge /healthz en HTTP ; les outils gRPC utilisent le service de santé standard (grpc.health.v1). Les deux doivent dire la même chose, sinon l’on finit par envoyer du trafic gRPC à une instance que l’HTTP croit en bonne santé.

Un keepalive cohérent

La politique du serveur (MinTime) et la configuration des clients mobiles doivent être réglées ensemble. Un client trop bavard se fait couper la connexion ; un serveur trop strict coupe les bons clients.

Les alternatives que j’ai écartées (pour l’instant)

grpcServer.ServeHTTPLa méthode officielle : un handler HTTP racine qui envoie à gRPC les requêtes HTTP/2 portant application/grpc, et le reste à Chi. Simple et élégant, mais marquée expérimentale, et elle passe par l’implémentation HTTP/2 de net/http, avec, selon la documentation elle-même, des performances et des fonctionnalités différentes.
ConnectRPCTrès séduisant : les services générés sont de simples handlers net/http, qu’on monte dans Chi, et ils parlent gRPC, gRPC-Web et le protocole Connect (lisible avec curl). Le panneau d’administration pourrait même appeler les mêmes méthodes que l’application. Si je repartais de zéro demain, je l’évaluerais sérieusement.
Un proxy devant (Envoy)Le routage par content-type est l’affaire de quelques lignes de configuration. Mais c’est un composant de plus à déployer, superviser et mettre à jour : exactement ce que la contrainte de coûts m’interdit.
grpc-gatewayGénère une API REST à partir des annotations du contrat. Précieux pour exposer les mêmes méthodes en REST. Mon REST, lui, sert d’autres besoins (admin, webhooks) : le générer depuis le contrat de l’application les mélangerait.

L’arborescence et l’image

L’arborescence du dépôt
backend/
├── proto/            # le contrat : .proto, buf.yaml, buf.gen.yaml
├── gen/              # code généré, jamais modifié à la main
├── cmd/server/       # main.go : un binaire, un port
├── internal/
│   ├── service/      # la logique métier, sans gRPC ni HTTP
│   ├── grpcapi/      # adaptateurs gRPC → service
│   ├── rest/         # Chi : admin, webhooks, mobile
│   └── store/        # accès aux données
└── Dockerfile
Dockerfile
FROM golang:1.26 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath \
    -ldflags="-s -w" -o /server ./cmd/server

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

La règle d’or de cette arborescence tient en une phrase : internal/service ne connaît ni gRPC ni HTTP. Les adaptateurs traduisent les messages protobuf et les requêtes JSON en appels de service, et les erreurs de service en codes gRPC ou HTTP. C’est ce qui permet d’ajouter une porte, ou d’en fermer une, sans toucher au métier. Et c’est aussi ce qui rendrait possible, un jour, de réécrire un service en Rust derrière le même contrat.

§ 6 · mise en œuvrestatut : en place

La stack tient en un binaire, un port et une poignée de bibliothèques. Il reste la dernière section d’un ADR, celle qu’on oublie toujours : les conséquences.

§ 7 · Conséquences
§ 7 · conséquences

Ce qu’on gagne, ce qu’on paie, ce qu’on surveille

Un ADR honnête se termine par ses conséquences, bonnes et mauvaises. C’est la section que relira celui qui voudra, un jour, défaire cette décision. Autant lui faciliter le travail.

Ce qu’on gagne

  • Un contrat unique entre le serveur et les deux applications, vérifié à chaque pull request.
  • Un seul binaire, un seul port, une seule image : une infrastructure minimale.
  • Une base de code que toute nouvelle recrue backend pourra lire très vite.
  • Un écosystème gRPC stable sous le cœur du produit.

Ce qu’on paie

  • Des garanties à construire par les tests et les linters, là où Rust les aurait données.
  • Une empreinte mémoire plus élevée qu’en Rust, et une latence moins régulière sous forte charge.
  • Une dépendance à cmux, une petite bibliothèque, au cœur du démarrage du serveur.
  • Le débogage du binaire gRPC, qui demande ses propres outils.

Ce qu’on surveille

  • La maturité de la crate gRPC officielle de Rust.
  • La latence du 99e centile et le temps passé dans le ramasse-miettes, mesurés en production.
  • L’apparition d’un traitement lourd dans le chemin critique.
  • ConnectRPC, comme remplaçant possible de cmux.

À vous de jouer

Je ne prétends pas avoir la seule bonne réponse, seulement la mienne, pour ce projet. Voici les questions que je me pose encore. J’aimerais beaucoup lire les vôtres.

Avez-vous déjà regretté un choix de langage ? Était-ce le langage, ou ce que vous attendiez de lui ?

Votre équipe écrit-elle ses ADR ? Qui les relit, deux ans plus tard ?

Si l’IA écrit de plus en plus de code, la lisibilité de Go devient-elle un avantage décisif, ou la rigueur du compilateur Rust un filet indispensable ?

gRPC sur mobile : l’avez-vous adopté, ou le JSON reste-t-il roi chez vous ? Pourquoi ?

Avec vos propres poids dans la pesée, qui gagne ?

Et vous, quel langage aimez-vous… sans jamais le choisir pour vos projets ?

Pour me répondre, écrivez-moi par e-mail ou sur LinkedIn. Je réponds avec plaisir, selon mon agenda, parfois avec un peu de retard, mais je réponds toujours.

Le mot de la fin
Le meilleur langage n’existe pas. Il existe le bon langage pour un projet, une équipe et un moment. J’aime Rust. Ce projet avait besoin de Go.
Rivel Babindamana
Développeur, octobre 2026

PS : le jour où un service de ce backend aura vraiment besoin de Rust, j’écrirai l’ADR-002. Et je vous le raconterai, avec plaisir.