Migrer de Go vers Rust
(corrode.dev)- Le passage de Go à Rust relève moins d’un choix motivé par le gain de vitesse que d’un déplacement des problèmes de
nil, de gestion des erreurs, de data races et de durée de vie des ressources vers des garanties à la compilation - Go a pour atouts une compilation rapide, des goroutines simples et un solide écosystème backend, mais Rust empêche davantage d’erreurs dès le système de types avec
Option,ResultetSend/Sync - Le borrow checker de Rust et
async/awaitentraînent un coût en courbe d’apprentissage et en ergonomie, et le temps de compilation doit clairement être considéré comme un recul par rapport à Go - Pour la transition, il est plus pertinent de commencer par des composants aux frontières nettes — comme des services hot path, des workers ou certains endpoints derrière une gateway — plutôt que par une réécriture complète
- Les bénéfices attendus se résument à une baisse de 20 à 60 % du CPU, de 30 à 50 % de la mémoire, une latence P99 plus stable, ainsi qu’une réduction des pannes liées aux déréférencements de
nilet aux conditions de concurrence
Le point central de la transition
- Le passage de Go à Rust porte moins sur la question de savoir si « Rust est plus rapide » que sur les garanties de correction, les compromis à l’exécution et les différences d’expérience développeur
- La comparaison se concentre sur les services backend, avec comme base les points forts de Go : petits binaires statiques, bibliothèque standard orientée réseau, et écosystème HTTP server, gRPC et base de données
- Certains éléments peuvent aussi s’appliquer aux outils CLI, aux firmwares embarqués ou aux moteurs de jeu, mais ce ne sont pas les cibles optimisées ici
- Comme documents de contexte, sont cités “Go vs Rust? Choose Go.” de 2017 et “Rust vs Go: A Hands-On Comparison” de l’équipe Shuttle
- Go est un langage à succès, mais certains choix de conception — comme l’usage généralisé de
nil, une gestion des erreurs reposant sur la discipline plutôt que sur le type système, ou l’absence longtemps prolongée de génériques — deviennent des points de débat majeurs face à Rust - Dans la JetBrains Developer Ecosystem Survey, Go est présenté comme un langage qui maintient une part de 17 à 19 % des développeurs actifs, tandis que Rust continue de croître mais reste à un niveau plus modeste
L’outillage
- Go et Rust disposent tous deux d’un outillage batteries included offrant, via une interface cohérente, build, tests, formatage, linting et gestion des dépendances
cargofournit plus largement, comme outillage de premier plan, les fonctions correspondant aux outils de Gogo.mod/go.sum→Cargo.toml/Cargo.lock: configuration du projet et manifeste des dépendancesgo get/go mod tidy→cargo add/cargo update: ajout et résolution des dépendancesgo build→cargo build: compilationgo run .→cargo run: exécution après buildgo test ./...→cargo test: testsgo vet ./...→cargo clippy: linter,Clippyétant bien plus prescriptif quevetgofmt/goimports→cargo fmt: formateur automatique sans configurationgolangci-lint run→cargo clippy -- -D warnings: mode de lint strictgo doc→cargo doc --open: génération et consultation de la documentation APIpprof→cargo flamegraph/samply: profiling CPUgovulncheck→cargo audit: vérification des vulnérabilités à partir d’une base de données d’avis de sécurité
- Dans Go, il est fréquent de combler les manques avec des outils tiers comme
golangci-lint,mockgen,airougoreleaser, alors que Rust couvre davantage de besoins de base via son écosystème principal - Même lorsqu’un crate externe est nécessaire, l’installation se fait en une fois avec
cargo install cargo-nextest, comme pourcargo watchoucargo nextest, et l’outil se comporte ensuite comme un outil natif, par exemplecargo nextest gofmtetrustfmtont surtout l’avantage de faire disparaître les débats de style en revue de code, plus que de satisfaire des préférences fines de mise en forme- Citation des Go Proverbs de Rob Pike : “Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.”
Différences fondamentales entre Go et Rust
- Les deux langages sont compilés, à typage statique, déployables sous forme de binaire unique et dotés d’un modèle de concurrence solide, mais ils diffèrent par l’étendue des garanties fournies par le compilateur et par le niveau de contrôle sur le comportement à l’exécution
- Les principaux points de comparaison sont les suivants
- Première version stable : Go en 2012, Rust en 2015
- Système de types : Go est statique et structurel, avec prise en charge des génériques depuis la 1.18 ; Rust est statique et nominal, avec génériques, traits et durées de vie
- Gestion mémoire : Go utilise un garbage collector concurrent à faible latence ; Rust repose sur la propriété et l’emprunt, sans GC
- Sûreté vis-à-vis du nul :
nilest omniprésent en Go ; Rust n’a pas de nul etOption<T>en est le substitut au niveau du type - Gestion des erreurs : Go utilise l’interface
erroretif err != nil { ... }; Rust utiliseResult<T, E>, l’opérateur?et un pattern matching complet - Concurrence : Go repose sur les goroutines et des channels de type CSP ; Rust sur
async/awaitavectokio, ainsi que des channels et des threads - Annulation : Go utilise
context.Contextpar convention ; Rust emploie des transmissions explicites et vérifiées par le type, commeCancellationToken - Data races : Go les détecte de manière probabiliste à l’exécution avec
-race; Rust les détecte à la compilation avecSend/Sync - Temps de compilation : Go est très rapide, tandis que Rust est lent, surtout sur les builds propres
- Runtime : Go embarque un runtime d’environ 2 Mo avec GC ; Rust n’a pas de runtime en dehors de
libc, ou peut être construit en statique complet avec MUSL - Taille de l’écosystème : Go compte environ 750 000+ modules, Rust 250 000+ crates
- En Rust, des vérifications comme la gestion de
nil, la propagation des erreurs, les data races, la durée de vie des ressources, l’annulation ou les génériques — qui en Go reposaient davantage sur les conventions, les outils et la détection à l’exécution — sont intégrées au système de types - Le
Mutex<T>de Rust n’autorise l’accès à la valeur interne qu’au travers du garde obtenu par.lock(), ce qui supprime du type même toute « voie où l’on oublierait de verrouiller » - Le même schéma se retrouve avec
Option,Result,&mut T,Send/Syncet les gardes RAII ; une fois l’habitude prise, le compilateur remplace une partie des vérifications mentales du développeur
Les limites de Go qui poussent à envisager Rust
- Comme Go est suffisamment rapide pour la plupart des charges backend, la principale raison d’évaluer Rust tient moins à la vitesse qu’à la lourdeur du traitement des erreurs, au risque de pointeurs
nilet à l’absence de fonctionnalités avancées du système de types comme les enum et les traits - Les interfaces Go ne remplacent pas suffisamment les traits de Rust, et la bibliothèque standard n’inclut pas de type
Set, ce qui impose des contournements idiomatiques commemap[T]struct{} -
Les paniques
nilen production- Un service Go peut fonctionner normalement pendant des mois puis déclencher une panique de goroutine sur un chemin de code précis à cause d’une vérification oubliée d’un pointeur
nil - Dans l’exemple,
Findrenvoie(*User, error)et, en cas de « not found »,errorvautnilmais la vérification deuserreste à la charge de l’appelant user.Account.Notify()peut planter siuserouAccountvautnil- Des linters comme
nilawayetstaticcheck, ainsi que les vérifications de l’IDE, en détectent une partie, mais ils sont opt-in, probabilistes et franchissent mal les frontières entre packages de façon fiable - Le
Option<T>de Rust empêche tout déréférencement sans traitement du casNone, ce qui élimine cette catégorie d’incidents
- Un service Go peut fonctionner normalement pendant des mois puis déclencher une panique de goroutine sur un chemin de code précis à cause d’une vérification oubliée d’un pointeur
-
Les data races que
-racen’a pas détectéesgo test -raceest un excellent outil, mais comme il s’agit d’un détecteur à l’exécution, il ne trouve que les races réellement déclenchées pendant les tests- En Go, un code où deux goroutines modifient une map sans verrou compile quand même, puis peut exploser en production sous charge
- En Rust, le partage d’état mutable entre threads exige des types implémentant
SendetSync, et tenter de partager une simpleHashMapentre threads ne compile pas - Cela force l’usage de
Arc<Mutex<...>>,Arc<RwLock<...>>ou de canaux, et les race conditions deviennent des erreurs de type - Paul Dix a cité explicitement l’élimination des data races comme motivation de la réécriture d’InfluxDB 3.0
- « [The main benefit is] fearless concurrency — eliminating data races essentially, which we had before. Really gnarly bugs in version 1 of Influx due to that. »
- Source : Paul Dix, Founder & CTO, InfluxData, Rust in Production
-
Un traitement des erreurs composable
- En Go,
if err != nil { return err }peut diluer la logique réelle d’une fonction, et le fait d’ajouter du contexte avecfmt.Errorf("doing X: %w", err)repose sur la discipline plutôt que sur une règle imposée par le compilateur - Dans un thread Lobste.rs, des développeurs Go expérimentés rétorquent que
errchecketgolangci-lintdétectent la plupart des oublis de traitement d’erreur, et que leif err != nilexplicite est plus lisible qu’une chaîne dense de? - Peter Bourgon présente le traitement explicite des erreurs en Go comme une valeur culturelle intentionnelle
- « I think that error handling should be explicit, this should be a core value of the language. »
- Source : Peter Bourgon, GoTime #91, cité dans le Zen of Go de Dave Cheney
- Le
Result<T, E>de Rust fait partie de la signature de type elle-même, donc il est impossible de l’oublier, et des enum définies avecthiserror::Erroret#[from]permettent d’obtenir la conversion d’erreurs et la vérification d’exhaustivité - Lorsqu’on ajoute un nouveau variant d’erreur, le compilateur indique quels
matchdoivent être mis à jour
- En Go,
-
Des génériques sans boxing
- Les génériques de Go 1.18 sont utiles, mais présentent des limites comme l’absence de méthodes avec paramètres de type, le GC shape stenciling et des caractéristiques de performance parfois surprenantes
- Les génériques de Rust sont monomorphisés : chaque instanciation produit un code spécialisé, sans coût à l’exécution
- Combinés aux traits, ils permettent des abstractions à coût nul
- C’est plus important dans l’infrastructure partagée — middleware, generic repository, decoder, parser — que dans le code des handlers, et Go revient souvent à
interface{}/anyet aux assertions de type dans ces zones
-
Une latence prévisible
- Le GC de Go est excellent, concurrent, à faible pause et bien optimisé pour les charges de service classiques, mais « low-pause » ne veut pas dire « no-pause »
- Dans les situations avec beaucoup d’allocations, la queue de latence P99 peut être moins bonne qu’avec une implémentation Rust qui n’alloue pas sur le hot path
- Dans les systèmes sensibles à la latence — trading, enchères en temps réel, proxy réseau, collecte à très haut débit — l’absence de pauses GC constitue un avantage réel
- Stephen Blum explique que Rust était nécessaire pour obtenir, à l’échelle de PubNub, la capacité de performance par dollar dont ils avaient besoin
- « Go is great at our scale, but we really need something that is going to give us the price-per-dollar performance capacity that we need, and Rust is going to get us there. That’s why basically everything is heading towards Rust these days. »
- Source : Stephen Blum, CTO, PubNub, Rust in Production
Correspondances Rust des patterns Go
- Le moyen le plus rapide de se familiariser avec Rust consiste à faire correspondre les patterns Go que l’on connaît déjà à leurs équivalents en Rust
- Un exemple plus long implémentant le même service backend dans les deux langages est disponible dans Shuttle comparison
-
Gestion des erreurs :
if err != nilvsResult<T, E>- En Go, après
os.ReadFile(path)etjson.Unmarshal, on renvoie une erreur enrichie de contexte viaif err != nil - En Rust, on utilise
fs::read_to_string(path)?,serde_json::from_str(&data)?, puisOk(cfg) - L’opérateur
?remplace le patternif err != nil { return err }et gère aussi la conversion de type siFrom<E1> for E2est implémenté - Le
#[from]dethiserrorprend en charge cette conversion de manière idiomatique
- En Go, après
-
Null :
nilvsOption<T>- En Go,
GetUser(id string) *Userrenvoienilsi l’utilisateur est introuvable, et si l’appelant faitfmt.Println(u.Name), cela provoque un panic lorsque la valeur estnil - En Rust,
get_user(id: &str) -> Option<User>renvoieSome(User)ouNone let user = get_user("123"); println!("{}", user.name);produit une erreur de compilation, caruserest unOption<User>et non unUser- Il faut traiter à la fois
Some(u)etNoneavecmatch get_user("123") - En Rust sûr, il n’existe pas de
nil, et une référence ne peut pas être nulle
- En Go,
-
Interfaces vs traits
- Les interfaces Go sont structurelles, et un type satisfait implicitement une interface
- Les traits Rust sont nominatifs et doivent être implémentés explicitement
- L’approche Go est bien adaptée au duck typing improvisé, tandis que l’approche Rust facilite le refactoring et la discoverability, et permet de retrouver avec
greples implémentations d’un trait donné - Les fonctions génériques avec trait bound, comme
fn handle<R: Reader>(r: R), couvrent la plupart des cas et, grâce à la monomorphisation, évitent le dispatch runtime - Pour stocker des implémentations hétérogènes nécessitant un dispatch runtime, on utilise
Box<dyn Trait>ouArc<dyn Trait>
-
Goroutine vs tâche async
- Le modèle de concurrence de Go est simple, comme avec
go doWork(ctx, input); les goroutines sont légères et le runtime les ordonnance au-dessus des threads OS - L’un des grands atouts de Go est qu’il n’existe pas de distinction syntaxique entre code séquentiel et code parallèle
- En Rust, pour les services backend, on utilise presque toujours
async/awaitsur l’executortokio - Les fonctions async renvoient un
Futureet ne s’exécutent pas tant qu’elles ne sont pasawaitouspawn - Le compilateur suit les
Send/Syncavant et après les points.await, et signale une erreur de compilation si une valeur non-Sendest conservée au-delà d’unawait - Comme il n’y a pas de préemption intégrée de type goroutine, faire tourner longtemps une tâche CPU-bound dans une tâche async peut affamer l’executor ; il faut la basculer vers
tokio::task::spawn_blockingourayon
- Le modèle de concurrence de Go est simple, comme avec
-
context.ContextvsCancellationToken- En Go, on transmet
context.Contextà tous les appels bloquants - Rust n’a pas de
context.Contextintégré, et l’équivalent le plus proche pour l’annulation esttokio_util::sync::CancellationToken - Les timeouts s’appliquent en enveloppant un future avec
tokio::time::timeout(dur, fut) - Les deadlines et les valeurs sont souvent transmises via des arguments explicites ou un span
tracing, plutôt qu’au moyen d’un unique objet context - Citation de Dave Cheney dans The Zen of Go :
- « Go n’a pas de moyen d’ordonner à une goroutine de s’arrêter. Il n’existe pas de fonction stop ou kill, et c’est une bonne chose. Si nous ne pouvons pas ordonner à une goroutine de s’arrêter, nous devons plutôt le lui demander, poliment. »
- En Go, cette « demande polie » est conventionnellement transmise via
context.Context; en Rust, ce seraCancellationTokenou un canalwatch, mais le compilateur peut signaler les oublis
- En Go, on transmet
-
Chaînes :
stringvsStringet&str- En Go,
stringest une slice d’octets UTF-8 ; lors d’une affectation, son en-tête est copié et les octets sous-jacents immuables sont partagés - Rust sépare cela en deux types
String: possède les données, est alloué sur le heap et extensible&str: vue empruntée sur d’autres données de chaîne, qui correspond dans la plupart des cas à un paramètrestringen Go
- La règle empirique consiste à accepter
&stren argument et à renvoyerStringlorsqu’on crée de nouvelles données - La séparation entre
&stretStringillustre en version condensée le modèle Rust « emprunter vs posséder »
- En Go,
Évaluation des génériques en Go
- Go a introduit les génériques avec la version 1.18 en mars 2022, soit 13 ans après le lancement du langage
- Les génériques sont utiles, mais sont jugés incapables d’apporter pleinement les avantages attendus en Rust, Haskell ou C++ moderne, tout en cumulant une bonne partie des inconvénients des systèmes de types génériques
-
La bibliothèque standard les utilise très peu
- Même 3 ans après l’introduction des génériques, la bibliothèque standard de Go les évite encore dans la plupart des cas
sort.Sliceaccepte toujours une closurefunc(i, j int) boolau lieu d’une contraintecmp.Orderedsync.Mapreste typé enany/any- Les helpers génériques existants se limitent à quelques packages, comme certains éléments sous
slices,maps,cmpetsync - La promesse de compatibilité Go 1 explique en partie la difficulté à remanier les API non génériques existantes, mais Go n’utilise pas les génériques comme outil principal à la manière de Rust
- En Rust, les génériques irriguent le langage depuis le début, avec
Option<T>,Result<T, E>,Vec<T>,HashMap<K, V>,Iterator,From/Into, ainsi que toutes les collections et les smart pointers
-
Pas de système de traits, uniquement des contraintes structurelles
- En Rust, les génériques sont liés aux traits, qui prennent en charge le polymorphisme ad hoc, les supertraits, les types associés, les blanket impl et la coherence
- Les contraintes de Go sont plus proches d’interfaces enrichies de l’opérateur
~pour l’appartenance à un ensemble de types - Go n’a ni hiérarchie de supertraits comme
trait Ord: Eq + PartialOrden Rust, ni types associés commetype Item;dansIterator, ni blanket impl commeimpl<T: Display> ToString for T - Go ne permet pas d’utiliser des méthodes avec paramètres de type, ce qui rend impossible une forme comme
func (s Set[T]) Map[U](<https://corrode.dev/learn/migration-guides/go-to-rust/f func(T>) U) Set[U] - Dès que l’abstraction dépasse le cadre de « fonctions qui opèrent sur un
Tarbitraire doté de quelques opérations », Go revient àany, aux assertions de type, à la génération de code et à la réflexion à l’exécution
-
Différences de stratégie d’inférence de types et d’implémentation
- Rust propage les informations de type dans l’ensemble de l’expression, y compris les closures, les chaînes d’itérateurs et l’opérateur
? - L’inférence de Go est plus limitée : elle déduit généralement les paramètres de type à partir des arguments de fonction, mais ne peut pas les inférer à partir du contexte de retour et exige souvent des arguments de type explicites au point d’appel
- Go a choisi une voie intermédiaire avec les GCShape stenciling and dictionaries, ce qui préserve des temps de compilation rapides, mais peut introduire une indirection à chaque appel de méthode sur un paramètre de type
- L’article de PlanetScale est cité pour illustrer ce point
- Rust produit du code machine spécialisé pour
Vec<i32>etVec<String>respectivement, sans dispatch dynamique à l’exécution - Le coût de la monomorphisation se paie en temps de compilation, et les deux langages optimisent des objectifs différents
- Rust propage les informations de type dans l’ensemble de l’expression, y compris les closures, les chaînes d’itérateurs et l’opérateur
-
Ne comble pas les trous du système de types
- En Rust, les génériques et les traits éliminent la plupart des situations qui exigeraient
Box<dyn Any>ou de la réflexion à l’exécution - Les génériques de Go n’éliminent ni
any, nireflect, ni les schémas dominants de génération de code dans les ORM, les décodeurs ou les mocks encoding/jsonutilise toujours la réflexion,database/sqlutilise toujoursany, etmockgengénère toujours du code- Les génériques de Go donnent l’impression d’un nouvel outil utile dans des cas restreints, tandis qu’en Rust ils jouent un rôle fondamental, au point que le langage s’effondrerait sans eux
- En Rust, les génériques et les traits éliminent la plupart des situations qui exigeraient
Écosystème backend Rust
- L’écosystème Rust converge lui aussi dans une certaine mesure vers des « choix par défaut » pour les services backend classiques
- Correspondances représentatives :
- Serveur HTTP : Go
net/http,chi,gin,echo,fiber→ Rustaxumsurhyper - Client HTTP : Go
net/http,resty→ Rustreqwest - gRPC : Go
google.golang.org/grpc+protoc-gen-go→ Rusttonic+prost - SQL : Go
database/sql,sqlc,sqlx,gorm→ Rustsqlx,sea-orm,diesel - Migrations : Go
golang-migrate,goose→ Rustsqlx migrate,refinery - JSON : Go
encoding/json,sonic,goccy/go-json→ Rustserde+serde_json - Logging : Go
log/slog,zerolog,zap→ Rusttracing+tracing-subscriber - Métriques : Go
prometheus/client_golang→ Rustmetrics+metrics-exporter-prometheus - Configuration : Go
viper,koanf→ Rustconfig/ config-rs,figment - CLI : Go
cobra,urfave/cli→ Rustclapderive - Erreurs : Go
errors,pkg/errors→ Rustthiserrorpour les bibliothèques,anyhowpour les binaires - Tests : Go
testing,testify,gomega→ Rust#[test]intégré,rstest,assert_matches - Mocking : Go
mockgen,moq→ en Rust, les fakes écrits à la main sont idiomatiques, maismockallest aussi utilisé - Tâches en arrière-plan : Go goroutines +
errgroup→ Rusttokio::spawn+JoinSet
- Serveur HTTP : Go
- Pour un service backend typique, la combinaison
axum+sqlx+tokio+tracing+serde+clapest présentée comme couvrant 90 % des besoins
Le borrow checker et la courbe d’apprentissage
- Il faut partir du principe qu’en passant de Go à Rust, on va se heurter à un mur
- Le runtime de Go gère à votre place la mémoire et l’aliasing, mais Rust déplace ces décisions dans le système de types, si bien que pendant les premières semaines, du code qui « devrait évidemment fonctionner » peut être refusé par le compilateur
- Schémas auxquels les développeurs Go se heurtent souvent :
- Références de longue durée de vie : en Go, conserver longtemps un
*Userextrait d’une map est naturel, mais en Rust, modifier la map est bloqué tant que cet emprunt reste vivant - Struct auto-référencées : en Go, on peut mettre des données et un itérateur sur ces données dans la même struct, mais en Rust, cela demande
Pin,ouroborosou une refonte - Partage d’état mutable entre goroutines : le schéma Go
mu sync.Mutex; data map[K]Vdevient en Rust quelque chose commeArc<Mutex<HashMap<K, V>>> - Retourner une référence depuis une fonction : les annotations de durée de vie entrent en jeu, un concept nouveau pour les développeurs Go
- Références de longue durée de vie : en Go, conserver longtemps un
- Il faut voir le borrow checker non pas comme un « gardien » gênant, mais comme un mécanisme qui révèle de vrais bugs
- Il élimine à la compilation les cas où une valeur est réutilisée après avoir été déplacée, où plusieurs threads touchent simultanément les mêmes données, où des pointeurs null ou pendants sont déréférencés, ou encore où une référence vit plus longtemps que la valeur
- Une fois le concept d’emprunt intériorisé, il cesse d’être un adversaire pour devenir un partenaire, et les développeurs Rust expérimentés disent en général qu’entre 4 et 12 semaines, le borrow checker devient une aide précieuse
- Le CTO de PubNub, Stephen Blum, a déclaré dans Rustacean Station que le premier mois lui donnait « l’impression de réapprendre à programmer », car il avait été forcé d’affronter le borrow checker et les durées de vie
- Ed Page, mainteneur de
clap, explique dans Rustacean Station: clap with Ed Page que le borrow checker lui a permis de se concentrer sur les problèmes de plus haut niveau et qu’il détectait aussi des points qui lui avaient échappé dans son analyse
Principales difficultés d’une transition vers Rust
-
Temps de compilation
- Il faut considérer les temps de compilation de Rust comme une régression nette par rapport à Go, car un build release propre d’un service de taille intermédiaire peut prendre plusieurs minutes, là où Go compile presque instantanément
- Les builds incrémentaux et
cargo checkrestent raisonnables, et les temps de compilation se sont améliorés d’année en année, mais l’écart avec Go se ressent - Dans la boucle d’édition, utilisez
cargo check, découpez en workspace quand cela devient utile, et gardez les crates riches en macros procédurales dans des crates séparées afin qu’elles ne soient recompilées qu’en cas de modification - Pour aller plus loin, on peut consulter des conseils pour réduire les temps de compilation de Rust
-
Le problème de la coloration asynchrone
- La séparation entre
async fnetfnen Rust est l’une des plus grosses régressions d’ergonomie lorsqu’on vient de Go - Les async trait sont stables depuis Rust 1.75, mais présentent encore des aspérités lorsqu’on les mélange au dispatch dynamique
- Dans certains cas, on finit par utiliser la crate
async-traitpour lisser ces problèmes
- La séparation entre
-
Un écosystème plus petit
- L’écosystème de crates Rust est en croissance et la qualité des bibliothèques est globalement élevée, mais Go reste en avance dans certains domaines proches du backend
- Les domaines où Go garde l’avantage incluent les opérateurs Kubernetes, les SDK des fournisseurs cloud et les drivers de base de données pour certains stockages de niche
- Avant d’acter une migration, il faut consacrer environ une journée à vérifier qu’il existe des alternatives Rust viables pour les bibliothèques dont vous dépendez
- Certaines équipes peuvent devoir mettre à jour une crate de validation de schéma XML abandonnée ou écrire elles-mêmes un client pour un protocole peu répandu
Stratégies d’intégration
- Une transition réussie de Go vers Rust relève moins d’une réécriture totale en une seule fois que de choix tactiques
- Victor Ciura, Principal Engineer chez Microsoft, explique dans Rust in Production qu’il s’agit non pas de « tout réécrire en Rust pour le plaisir », mais d’un choix tactique consistant à utiliser Rust lorsqu’un nouveau composant s’y prête mieux
-
1. Extraire le hot path en service distinct
- Si un service donné pose continuellement problème, la migration la moins risquée consiste à ne réécrire en Rust que ce service, derrière le même contrat d’API
- La cible peut être un service gourmand en CPU, sensible à la latence ou sujet à des problèmes de stabilité récurrents
- Les autres services Go continuent de communiquer en HTTP/gRPC et n’ont donc pas besoin de connaître le langage d’implémentation interne
- Jeff Kao, CTO de Radar, déclare dans Rust in Production que l’article de Discord sur son passage de Go à Rust a donné à Radar l’idée de tenter la même approche
-
2. Remplacer des sidecars ou des processus worker
- Les workers d’arrière-plan, consommateurs de files, pipelines d’ingestion et traitements batch CPU-bound constituent de bonnes premières cibles
- Ils disposent généralement de frontières d’entrée/sortie claires, comme une queue ou un topic, et ne partagent pas d’état in-process avec le reste du système
-
3. cgo est possible, mais pénible
- Il est possible d’appeler Rust depuis Go via cgo, et il existe même un bon guide sur le sujet
- En backend, ce n’est généralement pas recommandé
- La complexité du build et le surcoût FFI compensent souvent les avantages par rapport à une approche consistant à « monter un service Rust et le placer derrière un appel réseau »
- Cela peut être plus pratique pour des bibliothèques et des outils CLI
-
4. Appliquer le Strangler Pattern derrière une gateway
- Si vous avez une API gateway ou un reverse proxy, vous pouvez ne router que certains endpoints vers un nouveau service Rust et laisser le reste en Go
- Cela fonctionne particulièrement bien lorsqu’un seul bounded context, comme l’authentification, la recherche ou le paiement, se prête à servir d’unité de migration
- Ce schéma est appelé « strangler fig », car le nouveau service grandit autour du service existant jusqu’à le remplacer complètement
Conseils de migration sur le terrain
- Il faut commencer par un service aux frontières claires, et non par le service le plus central ou le plus fréquemment déployé
- Choisissez un service dont le contrat avec le reste du système est bien défini et dont le rayon d’impact est limité
-
Garder le même contrat d’API
- Si le service Go expose une API REST, le service Rust doit conserver les mêmes routes, le même format JSON et les mêmes wrappers d’erreur
- La migration est invisible pour les clients, et vous pouvez faire basculer progressivement le trafic via la gateway
-
Ne pas transposer littéralement les idiomes
if err != nil { return err }devient?- Le schéma « une goroutine par requête » ne se traduit en
tokio::spawnque lorsqu’il est réellement nécessaire axumtraite déjà les requêtes en parallèle- Les interfaces à une seule méthode deviennent généralement des trait bounds génériques plutôt que des
Box<dyn Trait>
-
Utiliser le compilateur comme pair programmer
- Les messages d’erreur du compilateur Rust sont en général de grande qualité, et si on les lit lentement, ils indiquent presque toujours la bonne réponse
- Les membres d’équipe qui souffrent le plus longtemps sont souvent ceux qui ne considèrent pas le compilateur comme un collaborateur, mais comme un adversaire
-
Investir tôt dans la formation
- Les migrations Rust menées « à côté » du reste se terminent souvent mal
- Il faut réellement dégager du temps d’apprentissage, via des ateliers, des cours en ligne ou des sessions de pair programming sur du vrai code
- Une fois l’équipe compétente, l’investissement initial est récupéré plusieurs fois
Domaines où Go reste pertinent
- Il n’est pas nécessaire de tout migrer vers Rust, et il existe des domaines où Go est particulièrement adapté
-
Outils natifs Kubernetes
- Pour les opérateurs, contrôleurs et CRD, l’écosystème reste très largement centré sur Go
-
Utilitaires CLI et outils de développement
- Leurs points forts sont une compilation rapide, une cross-compilation simple et un déploiement facile
-
Services de glue
- Pour une fine couche d’API, des proxys ou des convertisseurs de format, la part de boilerplate en Rust ne vaut pas toujours l’effort
-
Là où la vitesse de l’équipe prime sur les garanties absolues de correction
- Dans les domaines où il faut avancer vite, Go peut continuer à être le bon choix
- Jon Seager, VP of Engineering chez Canonical, explique dans Rust in Production que Go est un excellent choix pour les services réseau, que Canonical utilise beaucoup Go et que Juju est lui aussi une énorme codebase Go
- Une stratégie hybride est courante, et beaucoup d’équipes finissent avec un backend polyglotte : Go pour les services « ennuyeux », Rust pour ceux où la stabilité et les performances compensent l’effort supplémentaire
Améliorations attendues
- Les chiffres varient fortement selon la charge de travail : il faut donc les voir comme un ordre de grandeur, pas comme une promesse
- Fourchettes d’amélioration approximatives observées lors de migrations de Go vers Rust :
- Utilisation CPU : baisse de 20 à 60 %
- Go étant déjà efficace, l’effet est moins spectaculaire que lors d’un passage de Python à Rust
- Les gains viennent de l’absence de GC et de boucles plus resserrées
- Mémoire : baisse de 30 à 50 %
- Principalement grâce à l’absence de surcoût du GC et à un runtime plus léger
- Latence P99 : nettement plus régulière
- Les services Rust ont tendance à présenter une courbe plus lisse, avec moins de jitter provoqué par le GC qu’en Go
- Go s’est beaucoup amélioré depuis l’introduction de son GC faible latence, mais l’écart subsiste sous forte charge
- Incidents de production : c’est le domaine d’amélioration que les équipes rapportent le plus volontiers
- Des bugs comme les data races qui passent
go test -raceet arrivent quand même en production, les deref de nil ou les chemins d’erreur oubliés ne compilent tout simplement pas en Rust - Après une migration vers Rust, les rotations d’astreinte deviennent en général très ennuyeuses
- Des bugs comme les data races qui passent
- Utilisation CPU : baisse de 20 à 60 %
- Andrew Lamb, Staff Engineer chez InfluxData, raconte dans Rustacean Station: Rebuilding InfluxDB with Rust qu’après la réécriture d’InfluxDB, ils n’avaient plus à traquer des crashes, d’étranges race conditions multithreadées ou d’autres problèmes auparavant très chronophages
- Passer de Go à Rust a peu de chances d’apporter une amélioration de débit par 10 comme on peut parfois l’observer lors d’un passage de Python à Rust
- Les vrais bénéfices sont la réduction des « erreurs absurdes », des queues de latence plus plates et la capacité à s’étendre à d’autres domaines, comme l’embarqué ou la programmation système, avec le même langage
Remarques complémentaires
- Le système de types de Rust n’élimine pas tous les bugs de logique liés à la synchronisation, mais un type qui ne peut pas être partagé entre threads sans synchronisation ne compilera pas
- Le genre de problème où « on a oublié un verrou » et qui finit en corruption silencieuse des données peut être empêché par le système de types de Rust
- En Go,
stringest une séquence immuable d’octets, conventionnellement en UTF-8, mais cela n’est pas garanti au niveau du type - La correspondance la plus proche est Go
string↔ Rust&strpour une vue en lecture seule, et Go[]byte↔ RustVec<u8>pour un buffer mutable - En Rust,
Stringest la version possédée et extensible de&str, avec la garantie supplémentaire que le contenu est un UTF-8 valide - Pour plus de détails, voir Strings, bytes, runes and characters in Go
- Depuis Go 1.18, les fonctions génériques et les types génériques sont possibles, mais les paramètres de type sur les méthodes elles-mêmes n’ont pas été introduits
- Les chaînes d’itérateurs comme
(0..100).filter(|n| ...).collect()peuvent sembler peu familières aux développeurs Go, mais on peut aussi utiliser des bouclesforen Rust, et c’est souvent le bon choix pour du code ponctuel
Conclusion
- Le passage de Go à Rust est différent d’une transition vers Rust depuis Python ou TypeScript
- Les développeurs venant de Go connaissent déjà les avantages du typage statique et des langages compilés : il ne s’agit donc pas de quitter le typage dynamique ou un runtime lent
- Le compromis central consiste à abandonner
nilpour obtenir une codebase plus robuste, moins de pièges et un compilateur plus strict qui détecte davantage d’erreurs à la compilation - En contrepartie, la courbe d’apprentissage est plus raide
- Pour des services dont l’organisation dépend, qui exigent une forte disponibilité et qui sont critiques pour le business, comme les services fondamentaux, ce compromis vaut clairement le coup
- Pour d’autres services, Go peut encore rester la bonne réponse
- Le but d’une migration est d’affecter chaque problème au langage qui le résout le mieux
1 commentaires
Commentaires sur Hacker News
Migrer de C/C++ ou de Python vers Rust se comprend pour plusieurs raisons, mais pour un backend web, Go semble être un choix bien adapté
J’utilise presque exclusivement Rust, mais la dernière fois que j’ai travaillé sur un serveur web en Rust, je me suis dit que j’aurais dû prendre Go
L’article original souligne que la syntaxe de gestion des erreurs de Go est verbeuse, et c’est vrai. Rust avait le même problème, puis a ajouté la syntaxe
?qui renvoie la valeur d’erreur en cas d’échec. La gestion des erreurs en Go est le plus souvent une version développée de celaRust n’a pas de type d’erreur unifié, et ses principaux systèmes d’erreurs comme
io::Error,thiserrorouanyhowrendent la remontée dans la chaîne d’appels fastidieuseIl y a des choses qu’il est difficile d’ajouter plus tard si elles manquent dans un nouveau langage : type des constantes, type booléen, type d’erreur, type de tableau multidimensionnel, types de vecteurs·matrices de taille 2/3/4 et opérations standard, etc. Si rien n’est standardisé tôt, on perd ensuite énormément de temps à harmoniser plusieurs représentations d’un même concept
En dehors de la gestion des erreurs, c’est moins sensible pour le web, mais en calcul numérique, graphisme ou modélisation, devoir appliquer des opérations standard à des tableaux de nombres devient une vraie souffrance
Go a deux grands atouts pour les services web. Le premier, c’est ce que l’article appelle les goroutines, et le second, moins traité dans le texte, ce sont les bibliothèques. Go dispose de la plupart des bibliothèques nécessaires aux services web, souvent utilisées aussi en interne chez Google, donc éprouvées dans des conditions très dures. À l’inverse, les crates Rust sont moins mûres et manquent souvent de garantie qualité officielle
Rust dépend aussi encore de nombreuses bibliothèques C/C++ par rapport à Go, ce qui rend souvent plus problématiques la compilation croisée, les builds reproductibles et la génération de binaires statiques
Le défaut de Go, c’est que son garbage collector est trop simple. En cas de pics de latence, il reste peu de solutions en dehors d’une réécriture douloureuse
Les éléments cités ne sont que des façons courantes de l’utiliser, et utiliser simplement
Boxne pose aucun problème. C’est en gros similaire à ce que faitanyhow::ErrorCela dit, du côté de la bibliothèque standard, je trouve que Go s’en sort bien mieux que Rust
J’aime Rust comme langage et je l’utilise pour du firmware embarqué et des applications PC, mais pour le backend web, j’utilise encore Python. Rust n’a pas d’ensemble d’outils du niveau de Django ou Rails
Il existe des équivalents à Flask, mais pas l’écosystème solide de Flask. J’ai peu d’expérience avec Go, mais pour un backend web, je choisirais probablement Go plutôt que Rust, à cause de l’écosystème de bibliothèques et de frameworks
Et pour les raisons habituelles, je n’aime pas beaucoup Async Rust. L’écosystème web Rust impose presque partout l’asynchrone
io::Errorn’est qu’un des nombreux types qui l’implémentent, sans statut particulier. Les erreurs définies avecthiserrorimplémentent elles aussi ce traitanyhowsert simplement à dire commodément « n’importe quelle Error » lorsqu’on ne veut pas détailler dans le contrat d’API tous les types d’erreurs qu’une fonction peut produireRust permet plus facilement que Go d’écrire du code déterministe, ce qui est très utile quand on a besoin de tests de simulation déterministes et de tests basés sur les propriétés
J’ai récemment écrit en Go un outil de mirroring de données Postgres-to-Iceberg, https://github.com/polynya-dev/pg2iceberg, mais je l’ai porté en Rust parce que je voulais faire des tests de simulation déterministes sans me battre contre le runtime Go
Cela dit, si le domaine concerné n’est pas assez important pour justifier ce niveau de test, je choisirais toujours Go plutôt que Rust
Article lié : https://www.polarsignals.com/blog/posts/2024/05/28/mostly-ds...
Ça va peut-être sembler convenu et répétitif, mais mon principal reproche à Rust concerne la situation de la gestion des paquets, et je pense que c’est entièrement le résultat d’un certain état d’esprit chez les développeurs
J’aime l’ergonomie côté Rust. L’approche fonctionnelle des types de données est élégante. Mais je travaille en parallèle sur un projet Rust et un projet Go, et l’arbre de dépendances n’a rien à voir
Le projet Go repose presque entièrement sur la bibliothèque standard, alors que dans le projet Rust, j’ai seulement demandé
rusqlite(sqlite),clap(CLI),ratatui(TUI) ettauri(GUI), et j’ai déjà l’impression d’avoir plus de 400 dépendances.taurien est de loin le principal responsable, mais même sans lui on frôle encore la centaine, ce qui me paraît délirantS’il existait de meilleures alternatives de crates Rust, bien maintenues et raisonnables sur les dépendances, ce serait déjà beaucoup mieux, mais je n’en ai pas encore trouvé. Je ne veux simplement pas introduire un shai hulud dans mon système, alors que côté web Rust, on dirait que certains veulent faire de
cargoun équivalent denpmCela donne donc l’impression qu’il y a plus de dépendances qu’en réalité. Même séparées en crates distinctes, elles ont souvent le même mainteneur et font partie du même dépôt Git en amont
Cela dit, je suis d’accord avec l’impression générale. Rust a beaucoup de crates en version 0.x à moitié abandonnées, et il n’existe souvent pas de meilleure alternative
Et ensuite arrive
httplib3, puishttplib4Autrement dit, je préfère largement l’approche de Rust. Que je dépende de la bibliothèque standard ou d’une dépendance externe ne change pas grand-chose pour moi : dans tous les cas, cela reste une dépendance
Le fait que ce soit dans la bibliothèque standard et donc supposément de meilleure qualité ou mieux maintenu relève d’une autre question
Au final, tout dépend des ressources. Bien sûr, la bibliothèque standard peut en recevoir davantage, mais elle peut aussi devenir obèse et impossible à maintenir
rusqlite,clap,ratatuiettauriIl faut aussi voir que Tauri lui-même est composé de 14 crates, chacune apparaissant dans l’arbre de build
https://github.com/tauri-apps/tauri/blob/dev/Cargo.toml
Ratatui en compte 6
https://github.com/ratatui/ratatui/blob/main/Cargo.toml
Personne ne l’a « résolue », et je doute qu’il existe un jour une solution unique
Avec Go, il faut faire confiance aux auteurs de bibliothèques pour respecter correctement le versionnage sémantique, et on ne peut pas figer les versions. Personnellement, c’est quelque chose qui m’agace aussi beaucoup
Il existe quelques contournements : utiliser un SHA comme un hash de commit Git pour fabriquer une pseudo-version, ou recourir au vendoring, qui est un cache de dépendances connu. Mais le vendoring amène aussi ses propres problèmes de gestion de cache
J’ai dû utiliser un environnement virtuel Python ce week-end, et ça s’est mal terminé ; ça m’a rappelé pourquoi j’avais quitté Python
Le CPAN de Perl, Maven/Gradle de Java, les gems de Ruby, dep/glide/vgo/modules de Go, Cargo de Rust, npm/yarn de Node, tous ont des problèmes similaires
Les systèmes d’exploitation aussi : yum/rpm chez Redhat, apt chez Debian, snap chez Ubuntu, etc. Je ne comprends toujours pas particulièrement snap
Dans ce cas d’usage, garder le frontend en Go et ne faire passer que le backend en Rust pourrait peut-être avoir du sens
Ce texte paraît étrange parce qu’il essaie à la fois d’être un guide de migration et un plaidoyer pour Rust
En fin de compte, si l’on hésite entre Rust et Go, la question centrale revient presque entièrement à « veut-on un runtime managé ? » Une génération entière de programmeurs Rust s’est convaincue que les runtimes managés étaient mauvais et que leur absence constituait une fonctionnalité essentielle
Mais c’est manifestement faux. Il existe plus de domaines de programmation où l’on veut un runtime managé que de domaines où l’on n’en veut pas
Cela ne signifie pas pour autant que Go doive être le choix par défaut dans tous ces cas. Il existe aussi beaucoup de raisons subjectives de préférer Rust. Quand j’utilise Go,
matchme manque, maistokioet Async Rust ne me manquent pasLes deux sont des choix légitimes dans presque tous les cas où il n’est pas nécessaire de tordre artificiellement l’espace du problème. Écrire par exemple un module du noyau Linux en Go serait en revanche un choix bizarre
L’affrontement Rust contre Go ressemble à une marge étrange et embarrassante de notre secteur. Une grande partie de l’industrie construit très bien des systèmes entiers en Python ou en Node, et se moque des originaux qui se disputent pour savoir quel langage compilé à typage statique choisir. La vraie question, c’est Python contre Rust/Go, pas Rust contre Go
Mais de manière générale, les camps Rust et Go devraient unir leurs forces contre les méfaits du typage dynamique. Si les annotations de type sont désormais considérées comme une bonne pratique, n’est-ce pas l’aveu implicite qu’il y avait là un défaut ?
Même de bonnes annotations de type restent inférieures à l’inférence de types. L’inférence permet de laisser intacte une grande partie du code lors d’un changement de type, tout en empêchant les changements de type involontaires
J’aimerais simplement que TS ait un peu plus de runtime. La seule chose que j’envie à Python, c’est la facilité très naturelle avec laquelle on peut faire de la validation de schéma JSON sur des endpoints HTTP
Le passage obligé par Zod continue de m’irriter, et j’y vois un problème dû au dogmatisme de l’équipe TS
Les traces d’écriture LLM deviennent de plus en plus subtiles, mais restent encore assez visibles. Le mot genuine en particulier
Des phrases comme « This is the area where Go genuinely shines, and it’s worth being precise about why », « the lack of GC pauses is a genuine selling point », « Humans are genuinely bad at reasoning about memory » ou « There are cases where the borrow checker is genuinely too strict » en sont des exemples
Je ne pense pas que tout le texte soit généré par IA, mais plutôt qu’il a été aidé par IA. Si c’est le cas, l’auteur s’en est genuinely bien sorti
Comme personne d’autre ne semble le relever, cela n’a sans doute pas nui de manière significative au contenu, mais je trouve étrange que cela devienne si fréquent et si difficile à détecter
C’est vers « Go is clearly working for a lot of people, » que j’ai commencé à soupçonner une aide de l’IA. Bien sûr, ce n’est peut-être pas le cas, et je suis mauvais pour ce genre de détection
Plus qu’un indice précis, c’est ironiquement une impression. Si un texte « sonne » comme de l’écriture assistée par IA, je perds presque immédiatement mon intérêt, même s’il est correct par ailleurs
J’aimerais que les gens se sentent plus à l’aise pour écrire directement leurs propres idées comme elles leur viennent
it's worth being precise about ...me semble être une tournure bien plus typiquement IA que l’usage de genuinePar exemple, ce paragraphe me donne cette impression : « Go got generics in 1.18, and they’re useful, but the implementation has constraints (no methods with type parameters, GC shape stenciling, occasional surprising performance characteristics). Rust generics monomorphize, each instantiation produces specialized code with zero runtime cost. Combined with traits, this gives you real zero-cost abstractions. »
Chaque phrase dit quelque chose, chaque phrase est importante et remplit son rôle. C’est le genre d’écriture qu’on attend davantage d’un ouvrage très spécialisé ou d’un article scientifique que d’un billet de blog
Et cela rend justement la lecture plus difficile et plus ennuyeuse
Je ne m’attends pas à ce que les textes générés par LLM soient exempts de formules creuses. J’aimerais simplement que nous montrions tous un meilleur sens de l’édition, afin d’éviter de relire sans cesse la même voix
Pour un nouveau projet, rien n’empêche de l’écrire en Rust
Mais si le code existe déjà, fonctionne et génère des revenus, mieux vaut continuer en corrigeant dans le langage d’origine uniquement les parties qui doivent vraiment être réécrites
Améliorez le système de façon modeste et mesurable avec un langage que vous connaissez et une équipe en laquelle vous avez confiance. Tout le reste n’est qu’une guerre de religion stérile
J’aimais déjà Rust avant d’avoir lancé des benchmarks, mais la différence d’efficacité avec laquelle la plupart des LLM écrivent en Rust et en Go était bien plus grande que je ne l’imaginais. C’était encore plus visible avec des harnesses agentiques capables de corriger les problèmes d’environnement initiaux
En voyant cela, je suis devenu un partisan de Rust assez convaincu. J’ai obtenu de bons résultats en écrivant en Rust des outils de traitement batch appelés depuis une base de code existante, mais je n’ai pas encore tenté une migration complète en production
Les problèmes de Go évoqués dans l’article, en particulier autour de
nil, me semblent de plus en plus gérables avec une revue de code approfondie via Codex. Il vaut évidemment mieux que ces problèmes n’existent pas, mais pour des développeurs qui consacrent autant d’efforts à la revue et à la compréhension qu’à la conception et à l’implémentation, ce type de bug de sécurité devient de plus en plus optionnelLes données par langage sont ici : https://gertlabs.com/rankings?mode=agentic_coding
Rust force fortement l’utilisateur à rester sur une trajectoire bien définie. Codex finit toujours par produire quelque chose qui compile
L’inconvénient, c’est qu’au lieu d’échouer quand une approche idiomatique est parfois impossible, il peut produire une implémentation bête qui compile et satisfait quand même la demande
Les LLM écrivent du code plus vite que les humains, donc le temps passé à attendre la compilation pèse relativement plus lourd. À partir d’une certaine taille de projet, disons au-delà de 100 000 lignes, une compilation Rust environ 10 fois plus lente commence à devenir un vrai goulet d’étranglement
Si l’on écrit une infrastructure centrale, ce coût peut valoir la peine, mais pour des services internes non exposés sur Internet, la vitesse de développement peut être plus importante
Je pense aussi que les compilations lentes affectent la vitesse de développement humaine, mais curieusement les développeurs cherchent très rarement à la quantifier
Si la verbosité est le principal obstacle, ceci, prévu pour Go 1.28, devrait la réduire fortement
https://github.com/golang/go/issues/12854#issue-110104883
La formule « service dont l’organisation dépend, nécessitant une forte disponibilité et critique pour l’activité » est amusante
C’est particulièrement vrai lorsque ce service Rust tourne sur Kubernetes
J’utilise déjà Rust et je n’ai pas d’expérience avec Go, donc ce texte n’est peut-être pas vraiment fait pour moi
Mais un point me gêne : dire qu’en Rust les data races sont « détectées à la compilation » me semble au moins un peu exagéré
Cette formulation peut laisser croire que Rust gère aussi le starvation sur mutex ou d’autres problèmes de concurrence. En pratique, ce n’est pas le cas
Je sais que data race est un terme formel au sens étroit, mais je pense quand même qu’on pourrait l’écrire de façon plus claire