Accueil / Articles / Rust ou Go pour les services backend : ne payer que pour les garanties dont vous avez besoin

Rust ou Go pour les services backend : ne payer que pour les garanties dont vous avez besoin

Pourquoi la sécurité et la vitesse de Rust ne résolvent que rarement les véritables goulots d’étranglement des équipes API, comment Go optimise la maintenabilité, et quand Rust est le choix idéal par défaut pour les backends.

2501 mots

Rust fait partie des langages les plus impressionnants de la dernière décennie, et c’est précisément pour cette raison qu’il mérite une étude approfondie avant de devenir la langue de prédilection des équipes backend. La plupart des services backend ne sont pas limités par les qualités que Rust met en avant ; ils le sont plutôt par la vitesse à laquelle un groupe de personnes en évolution peut les comprendre et les modifier en toute sécurité. Cet article compare Rust et Go sous cet angle, montre où apparaissent réellement les inconvénients de chaque langage, et vous offre une méthode claire pour déterminer quand les avantages de Rust justifient son utilisation.

Un scénario à prendre en compte

Imaginez une équipe qui implémente une fonctionnalité backend ordinaire en Rust. Le code résultant est plus sûr, plus rigoureux et bénéficie de garanties plus solides en temps de compilation que l’équivalent en Go n’aurait pu l’être. De plus, son développement prend nettement plus de temps, déclenche de longues discussions sur les types et les durées de vie, et transforme un changement routinier en une discussion sur la conception. La même fonctionnalité en Go aurait été livrée rapidement et serait facile à comprendre pour n’importe qui dans l’équipe.

Aucun de ces résultats ne signifie que l’un des langages est mauvais. Cela veut dire que le coût d’un outil doit être évalué en fonction du problème auquel il est appliqué.

L’excellence ne revient pas à la même chose que l’adaptation

Rust a changé la façon dont l’industrie perçoit la sécurité, les performances et la correction sans avoir recours à un collecteur de déchets. Cette réalisation est réelle, et c’est ce qui lui vaut le respect qu’il suscite.

Mais la plupart des équipes backend ne travaillent pas sur des moteurs de stockage, des noyaux, des navigateurs, des moteurs de jeu, du firmware embarqué ou des infrastructures sensibles en matière de sécurité, où chaque allocation et chaque limite mémoire constituent une préoccupation majeure. La plupart d’entre elles développent des API. Leur travail quotidien consiste à transférer du JSON entre Postgres, Redis, Kafka, S3, des passerelles de paiement, des services de notification, des systèmes internes et des API tierces qui tombent en panne de manières créatives au pire moment.

Ce travail n’est pas glamour : règles métier, tentatives de réessai, journalisation, tableaux de bord, déploiements, migrations, et ingénieurs qui tentent de maintenir la production stable. Dans un tel environnement, Rust peut constituer une excellente solution à une question que l’équipe ne se posait même pas.

Se poser la bonne question

La manière habituelle de formuler le problème est biaisée. Un camp affirme que Go est trop simpliste ; l’autre prétend que Rust est trop complexe. Ces deux observations sont exactes, mais elles manquent toutes deux le cœur du problème.

« Le Rust est-il meilleur que le Go ? » est une question trop vague pour y répondre. Une tronçonneuse est plus efficace qu’un couteau de cuisine pour abattre des arbres, mais personne n’en utilise un à table. La comparaison pertinente concerne ce pour quoi chaque langage est optimisé :

  • Rust offre puissance et précision, et il cherche à éliminer des catégories entières d’erreurs avant même que le programme ne s’exécute.
  • Go offre retenue et compréhension rapide, et il est conçu en partant du principe que des ingénieurs ordinaires sous pression de délais normaux maintiendront le code.

Ce deuxième principe a plus d’importance qu’il n’y paraît au premier abord.

Qu’est-ce qui change lorsque la maintenance entre en jeu ?

De nombreux développeurs Go n’ont rien contre Rust. Certains l’admirent, d’autres le codent, d’autres encore souhaitent l’apprendre, et beaucoup reconnaissent volontiers qu’il s’agit du choix le plus fiable pour des tâches sérieuses de bas niveau. Le ton change lorsque le sujet passe de la conception du langage à la maintenance à long terme des systèmes backend.

À ce stade, personne ne conteste le pouvoir de Rust. La question devient alors de savoir si une équipe backend typique doit assumer les coûts liés à Rust pour résoudre des problèmes que ses services ne rencontrent généralement pas.

C’est là que Go devient un concurrent redoutable. Non pas parce qu’il est plus avancé, ce qui n’est pas le cas. Ni parce que son système de types est plus riche, ce qui n’est pas non plus le cas. Et certainement pas parce qu’il fait se sentir intelligents les développeurs ; il a plutôt tendance à faire l’inverse. Go gagne parce qu’il reflète une vérité dérangeante concernant les organisations logicielles : la plupart des équipes n’ont pas besoin de code plus expressif. Elles ont besoin de code que davantage de personnes puissent modifier sans en avoir peur.

Le goulot d’étranglement n’est que rarement le CPU

Les ingénieurs backend aiment croire que leur système n’est qu’à une optimisation de l’excellence. C’est une histoire flatteuse, mais généralement fausse.

La plupart des backends lents ne le sont pas à cause du moteur de langage. Ils le sont parce qu’une requête est inefficace, un cache fournit des données obsolètes, le réseau est peu fiable, une file d’attente est en surcharge, une dépendance est instable, ou un flux métier combine cinq opérations qui auraient dû être indépendantes. Un langage plus rapide ne résout aucun de ces problèmes.

La partie difficile de l’ingénierie backend n’est généralement pas de faire exécuter des instructions par la machine. C’est plutôt d’aider un groupe de personnes à comprendre le système suffisamment en profondeur pour qu’elles puissent le modifier sans le casser. Le diagramme ci-dessous illustre ce point en indiquant où se situent généralement les véritables obstacles :

A Normal Backend Team's Real Bottleneck

          CPU
           |
           |   usually not here
           v

    -----------------
    |  application  |
    -----------------
       |     |     |
       v     v     v

  Postgres  Redis  Kafka
       |       |      |
       v       v      v

  unclear ownership
  changing product rules
  missing observability
  slow code reviews
  fear of refactoring
  tired on-call engineers

The machine was rarely the hard part.
The humans were.

Cela explique comment Rust peut l’emporter sur le plan technique tout en restant un mauvais choix de départ pour de nombreuses équipes backend. Rust mise sur la correction là où le code rencontre d’autres codes. Go, quant à lui, optimise fréquemment pour sa survie au sein de l’équipe. Ce contraste résume toute la discussion en une seule phrase.

La simplicité de Go est une contrainte délibérée

func CreateOrder(w http.ResponseWriter, r *http.Request) {
    var req CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, "invalid request", http.StatusBadRequest)
        return
    }
    order, err := service.CreateOrder(r.Context(), req)
    if err != nil {
        writeError(w, err.Error(), http.StatusInternalServerError)
        return
    }
    writeJSON(w, order)
}

Nul ne qualifierait cela de futur de la programmation, pourtant presque tous les développeurs backend peuvent le comprendre immédiatement. Un ingénieur junior peut le suivre ligne par ligne. Un réviseur expérimenté peut l’approuver en une minute. Un nouvel employé peut comprendre le flux de contrôle sans explication supplémentaire. Quelqu’un qui débogue un incident peut voir exactement où une requête entre, où elle peut échouer et où elle sort.

Cette lisibilité n’est pas un avantage mineur ; dans de nombreuses organisations, c’est ce qui compte le plus. Ce fragment montre également à quel point les problèmes de Go sont visibles : renvoyer directement err.Error() au client peut divulguer des détails internes tels que les messages de la base de données, et dans un service réel on enregistrerait l’erreur puis on enverrait un message générique. La faille est facile à repérer précisément parce que rien n’est caché.

L’intérêt de limiter les décisions ingénieuses

L’expérience avec des systèmes à long terme a tendance à éroder l’admiration pour l’ingéniosité. Ce qui mérite le respect, c’est un code peu élégant présentant des points de défaillance évidents, un code qui n’exige pas que son auteur initial le explique.

Go est peu attrayant d’une manière utile : il limite le nombre de décisions complexes qu’une équipe peut prendre. Cela ressemble à une critique tant que l’on n’a pas eu à gérer un ensemble de codes rempli de décisions complexes. Chaque abstraction semblait judicieuse au moment de son introduction. Chaque outil générique avait une bonne raison d’exister. Chaque choix de framework était soutenu par des arguments convaincants. Puis les gens passent à autre chose, les exigences changent, et le code devient un ensemble de décisions prises par le passé que personne ne comprend vraiment.

La simplicité délibérée de Go s’oppose à cette tendance. Elle n’y parvient pas toujours. Un code Go de mauvaise qualité est courant : gestion redondante des erreurs, modèles de domaine simplistes, état global, courses de données, utilisation d’interfaces là où elles ne sont pas nécessaires, ainsi que l’utilisation généralisée de context.Context dans les threads sans une véritable compréhension du mécanisme d’annulation. La différence réside dans le fait que le désordre propre à Go est généralement visible, tandis que celui de Rust peut être bien plus sophistiqué. Ce n’est pas une faille de Rust ; c’est ce qui se produit lorsque un langage offre aux ingénieurs compétents plus de liberté pour exprimer leurs capacités.

Lorsque des tâches simples prennent une forme complexe

C’est là que Rust peut devenir problématique pour les équipes backend. La tâche elle-même peut être triviale, mais les types qui la entourent ne le sont pas nécessairement. La fonction ci-dessous traite un lot d’éléments séquentiellement, en attendant un gestionnaire asynchrone pour chacun d’eux et en s’arrêtant à la première erreur :

use std::future::Future;
trait Processable {
    type Output: Send + 'static;
}
async fn process_batch<F, Fut, T, E>(
    items: Vec<T>,
    handler: F,
) -> Result<Vec<T::Output>, E>
where
    F: Fn(T) -> Fut + Send + Sync + Clone + 'static,
    Fut: Future<Output = Result<T::Output, E>> + Send + 'static,
    T: Processable + Send + 'static,
    E: Send + 'static,
{
    let mut output = Vec::new();
    for item in items {
        output.push(handler(item).await?);
    }
    Ok(output)
}

La logique consiste en une boucle simple. Cependant, la signature doit préciser que le gestionnaire est appelable, clonable et sûr à envoyer et partager entre threads ; que le futur qu’il retourne est de type Send et 'static ; et que les types d’éléments et d’erreurs respectent les mêmes contraintes. Ce n’est pas un Rust artificiel ou problématique. Lorsque le code asynchrone, les gestionnaires génériques, les contraintes partagées, les tâches démarrées, les types d’erreurs et les durées de vie se combinent, Rust exige que vous indiquiez explicitement ce que d’autres langages backend laissent implicite.

Cette explicitité a une véritable valeur, et c’est parfois précisément ce dont un système a besoin. Cependant, elle n’est pas gratuite. Le coût se manifeste lors de l’intégration des nouveaux membres, lors des revues de code, et chaque fois qu’une fonctionnalité qui semble simple du point de vue du produit s’avère complexe du point de vue du système de types. Il apparaît également lorsque l’ingénieur consacre plus d’efforts à convaincre le compilateur d’accepter une solution qu’à se demander si cette solution doit exister au tout début.

Mieux, mais mieux en quoi ?

Les partisans de Rust affirment que ces frictions permettent d’obtenir de meilleurs systèmes, et ils ont parfois raison. Une équipe backend devrait se poser une question plus précise : mieux dans quelle dimension ?

  • Sécurité mémoire : possiblement, bien que Go soit également sûr en termes de mémoire, à l’exception des courses de données et de l’utilisation explicite de unsafe.
  • Prestations : souvent.
  • Prévention de certains bugs de concurrence : oui, dans de nombreuses situations.
  • Gérer les tâches courantes de livraison au sein d’une équipe aux compétences variées pendant trois ans : ce n’est pas obligatoire.
  • La dernière dimension est celle sur laquelle les équipes backend sont principalement évaluées, et c’est aussi là que l’avantage de Rust n’est pas automatique.

    C’est la maintenance, et non l’écriture du code, qui représente le véritable coût

    À ce stade, le choix du langage relève moins de l’élégance que de la durabilité sociale. Quelques questions permettent de le comprendre :

    • Le prochain ingénieur peut-il comprendre ce code ?
    • L’équipe peut-elle le refactorer progressivement ?
    • Un ingénieur de garde fatigué peut-il le modifier en toute sécurité sans avoir à mémoriser l’ensemble du graphe de types ?
    • À quelle vitesse un nouvel employé devient-il productif ?
    • Le système tient-il la route en moyenne sur plusieurs jours, et pas seulement dans des conditions idéales ?

    Go a tendance à répondre plus favorablement à ces questions. Non pas parce que les développeurs Go sont plus compétents, ni parce que le code Go est intrinsèquement propre, mais parce que ce langage maintient une surface d’interaction réduite et laisse moins de places où la complexité peut se cacher. C’est aussi pourquoi certains ingénieurs le trouvent frustrant. Go ne flatte pas ses utilisateurs. Rust peut vous faire sentir que vous construisez quelque chose d’important, tandis que Go peut vous donner l’impression de faire de la plomberie.

    L’ingénierie backend consiste principalement en des tâches de plomberie. L’eau doit couler, les tuyaux doivent être faciles à localiser, et la personne suivante doit pouvoir remplacer une vanne sans avoir d’abord à connaître l’histoire complète du bâtiment.

    Où Rust est clairement l’outil idéal

    Rien de tout cela ne signifie pour autant que Rust soit indispensable partout. Lorsque les performances, la sécurité mémoire et un contrôle de bas niveau sont essentiels à ce que vous développez, Rust mérite une réflexion sérieuse. Si les pannes sont inacceptables, si les bugs liés à la sécurité mémoire représentent un risque pour la sécurité, ou si la latence est un critère déterminant et non simplement une mesure, Rust peut être le choix le plus judicieux possible.

    Les proxies, les bases de données, les environnements d’exécution des langages, les outils de sécurité, les systèmes embarqués, les réseaux à haute performance, les outils de développement ainsi que certains services d’infrastructure relèvent tous de cette catégorie. Dans ces cas, choisir Rust constitue une décision d’ingénierie mûrement réfléchie.

    Cependant, une API backend standard ne devient pas plus mature en étant écrite en Rust. Parfois, cela la rend même plus coûteuse. Les équipes adoptent Rust pour des raisons valables, mais elles le font aussi parce que Go leur semble trop simple, Java trop orienté entreprise, Python trop laxiste, tandis que Rust représente selon elles le choix approprié pour des ingénieurs sérieux. Ce n’est pas un jugement d’ingénieur : c’est une insécurité esthétique déguisée en décision de programmation système.

    Un moyen rapide de se décider

    Rust constitue un choix par défaut approprié lorsque la plupart des critères suivants sont remplis :

    • le service fait partie de l’infrastructure, où les performances ou le contrôle de la mémoire font partie intégrante du produit,
    • L’équipe dispose déjà de plusieurs ingénieurs Rust expérimentés et peut en embaucher davantage,
    • une panne ou une faille de sécurité liée à la gestion de la mémoire entraîne des conséquences graves sur le plan de la sécurité ou des affaires.

    Go, ou un autre langage privilégiant la lisibilité, est le choix par défaut plus sûr lorsque la plupart des critères suivants sont remplis :

    • le service transfère principalement des données entre bases de données, files d’attente et API,
    • l’équipe dispose d’une expérience variée et connaît un turnover régulier,
    • les principaux risques sont une responsabilité floue, des exigences changeantes et une faible capacité d’observation, plutôt qu’un débit brut élevé.

    En résumé

    La brillance de Rust n’a jamais été remise en question. Ce qui compte, c’est de savoir si c’est précisément cette brillance qui manque à votre équipe backend.

    Si l’équipe est composée d’ingénieurs Rust compétents travaillant sur des infrastructures où les garanties offertes par Rust correspondent directement aux risques, choisissez Rust sans hésiter ; il est juste d’utiliser l’outil le plus adapté lorsque la tâche l’exige. Si l’équipe développe des services conventionnels qui transmettent des données entre des bases de données, des files d’attente, des API, des tableaux de bord et des outils internes, choisir Rust peut refléter davantage un goût coûteux qu’un niveau de maturité élevé.

    Go n’est pas préférable parce qu’il est plus puissant. Pour de nombreuses équipes backend, il l’est plutôt parce qu’il est plus facile à utiliser : il est plus simple à lire, à recruter des développeurs pour, à auditer et à déployer, ainsi qu’à maintenir sans particularités une fois l’enthousiasme initial retombé. Être sans particularités n’est pas un échec ; c’est ce dont ont besoin les systèmes en production bien après que l’excitation initiale soit passée.

    Les échecs qui entraînent réellement la chute des équipes backend proviennent rarement d’un manque de vérificateur d’emprunt. Ils découlent de frontières de service floues, de tentatives de réessai qui ne sont pas idempotentes, de bases de données qui sont devenues silencieusement la véritable API, de files d’attente ayant absorbé tous les raccourcis de conception, de journaux ne racontant qu’une partie de l’histoire, ainsi que d’architectures conçues pour une équipe idéalisée qui n’a jamais existé. Rust empêche de nombreuses catégories d’erreurs, mais il ne peut pas éviter l’erreur de choisir la puissance au lieu de la clarté lorsque c’est ce dont l’équipe a besoin. C’est pourquoi Rust constitue un mauvais choix par défaut pour la plupart des tâches backend : non pas parce qu’il est faible, mais parce que son excellence a un coût élevé lorsque le problème de fond concerne principalement les personnes.

    Lectures complémentaires