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.
- 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 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.
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 :
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 :
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é :
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 contratREST, 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 :
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 :
- Des messages compacts ? Le JSON répète le nom de chaque champ, dans chaque message.
- Un contrat partagé par le serveur et les deux applications ? OpenAPI existe, mais il est facultatif, et souvent écrit après coup.
- Un serveur qui prévient quand quelque chose change ? Par défaut, c’est l’application qui doit demander.
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 FacebookUne 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 GoogleOn 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é :
À 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.
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 reste tient dans un tableau. Sur presque chaque ligne, gRPC fait par construction ce que REST demande d’ajouter :
| Pour l’application mobile | REST | GraphQL | gRPC |
|---|---|---|---|
| Des messages compacts Batterie | JSON, texte | JSON, seulement les champs demandés | Binaire compact |
| Une connexion pour tous les appels BatteriePerformance | Possible, en HTTP/2 | Possible, en HTTP/2 | Toujours : HTTP/2 est son transport |
| Le serveur qui pousse Batterie | À ajouter (SSE, WebSocket) | Abonnements, souvent sur WebSocket | Flux 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ême | Intégrées au protocole |
| Dans un navigateur | Natif | Natif | Via gRPC-Web ou Connect |
| Lire et tester à la main | curl, partout | Bons outils dédiés | Outils 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 :
curl ne suffit plus. Il faut grpcurl, la réflexion activée sur le serveur, ou un client graphique qui comprend gRPC.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.
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 :
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 :
- Au premier plan
Watchest ouvertLe serveur pousse ce qui change. Fini le « y a-t-il du nouveau ? » toutes les trente secondes.
- 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.
- Au retour
Sync, puisWatchUn seul aller-retour rattrape ce qui a été manqué, et le flux reprend.
Pourquoi ce mélange pèse sur le choix du langage
C’est ici que le protocole rattrape le langage :
- gRPC est le cœur, donc son écosystème devient le critère numéro un. Les deux langages ont une bibliothèque gRPC. La vraie question : est-elle officielle, complète, et sera-t-elle encore la même dans trois ans ?
- REST est secondaire et banal, donc il ne départage presque rien. Les deux langages servent très bien du JSON. C’est un critère qu’on vérifie, pas un critère qui décide.
- Un seul port, donc il faut marier les deux proprement. Un seul service à déployer, un seul certificat, une seule sonde de santé, une seule ligne sur la facture : c’est la contrainte de coûts qui parle. Il faut savoir servir gRPC et REST sur la même adresse, sans proxy de plus.
§ 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 optionsGo 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 ?
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 pasnil.- Le
if err != nilré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.
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.
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 }
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.
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èresLes é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 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.tonic, 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 :- Jusqu’ici
tonicLa bibliothèque de référence, portée par la communauté.
- Mai 2026tonic rejoint le projet gRPC
Et passe en maintenance : corrections uniquement.
- À venirUne crate officielle
grpcAvec 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énet/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.axum, 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é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.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éesunsafe 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.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 RustGOMEMLIMIT 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é.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 GoGOOS, 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.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 GoL’é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ère | Go | Rust | Pour ce projet |
|---|---|---|---|
| Écosystème gRPC | Officiel, complet, stable | Très bon, en transition vers l’officiel | Go |
| REST | net/http, Chi | axum, actix-web | Égalité |
| Un seul port | cmux, ServeHTTP, Connect | tonic + axum sur tower | Égalité |
| Sécurité | Petite surface, bon outillage | Garanties du compilateur | Égalité |
| Mémoire, latence | Ramasse-miettes, bien réglé | Pas de collecte, très économe | Rust |
| Boucle de retour | Compilation quasi instantanée | Compilation longue | Go |
| Exploitation | Simple, profilage intégré | Bonne, plus exigeante | Go |
| Équipe de demain | Vite productive | Plus longue à former | Go |
| Modélisation du métier | Types modestes | Énumérations, Option, Result | Rust |
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.
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écisionGo, sans regret, mais pas sans hésiter
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
- 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-goest 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. - 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.
- 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.
- 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.
govulncheckfait le reste. - 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.
- 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.
go test -race dans l’intégration continue, sur toute la suite de tests, et des tests de charge qui l’activent aussi.staticcheck et golangci-lint, l’analyse nilaway d’Uber contre les pointeurs nuls, et des machines à états écrites explicitement, avec un test par transition.GOMEMLIMIT réglé sur la taille du conteneur, pprof en production, et des allocations surveillées sur les chemins les plus fréquentés.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 :
- Un service qui calcule vraiment. Si un jour le chemin critique contient du traitement lourd (médias, cryptographie intensive, moteur de règles en temps réel), ce service-là sera écrit en Rust.
- Une équipe qui parle déjà Rust. Avec deux ou trois développeurs Rust confirmés dans l’équipe, le critère du recrutement s’effondre, et la pesée change de camp.
- Une crate gRPC officielle mûre en Rust. C’est la case à cocher de la pesée. Le jour où elle sera stable et complète, le critère qui a tout fait basculer deviendra une égalité.
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.
.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 pour | Parce que |
|---|---|---|
| Une API backend, des microservices, une équipe qui grandit | Go | Lisibilité, vitesse d’intégration, écosystème cloud et gRPC. |
| Un MVP à livrer vite, avec une équipe généraliste | Go | La boucle de retour et la courbe d’apprentissage priment. |
| Un outil en ligne de commande, un outil d’infrastructure | Les deux | Deux excellents choix ; c’est l’équipe qui décide. |
| Un proxy, un moteur, un service à latence critique | Rust | Pas de collecte, une latence régulière, chaque cycle compte. |
| De l’embarqué, du système, du WebAssembly | Rust | Pas de runtime lourd, contrôle fin de la mémoire. |
| Une bibliothèque partagée entre iOS et Android | Rust | Un 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 œuvreLa 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.
Vue d’ensemble : un port, deux serveurs, une logique
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.
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
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.
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.
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.
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.
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é.
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.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.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.L’arborescence et l’image
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
DockerfileFROM 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équencesCe 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 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.
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.