1 Punkte von GN⁺ 3 시간 전 | 1 Kommentare | Auf WhatsApp teilen
  • Der Wechsel von Go zu Rust ist weniger eine Entscheidung für mehr Geschwindigkeit als dafür, Probleme wie nil, Fehlerbehandlung, Data Races und Ressourcenlebensdauer in Compile-Time-Garantien zu überführen
  • Go punktet mit schnellen Compile-Zeiten, einfachen Goroutines und einem starken Backend-Ökosystem, aber Rust verhindert mit Option, Result und Send/Sync mehr Fehler bereits im Typsystem
  • Rusts Borrow Checker sowie async/await bringen eine Lernkurve und Kosten bei der Benutzbarkeit mit sich; auch die Compile-Zeiten sind gegenüber Go klar als Rückschritt zu sehen
  • Für den Umstieg eignet sich eher eine Strategie, die mit klar abgegrenzten Komponenten beginnt – etwa Hot-Path-Services, Worker oder einzelne Endpunkte hinter einem Gateway – statt mit einer vollständigen Neuschreibung
  • Die erwarteten Effekte lassen sich als 20–60 % weniger CPU, 30–50 % weniger Speicher, flachere P99-Latenzen sowie weniger Ausfälle durch nil-Dereferenzierungen und Race Conditions zusammenfassen

Fokus des Wechsels

  • Der Wechsel von Go zu Rust ist weniger eine Frage von „Ist Rust schneller?“ als eine Abwägung von Korrektheitsgarantien, Runtime-Kompromissen und Unterschieden in der Developer Experience
  • Im Zentrum des Vergleichs stehen Backend-Services, wobei Go mit kleinen statischen Binärdateien, einer auf Netzwerke ausgerichteten Standardbibliothek und einem Ökosystem für HTTP-Server, gRPC und Datenbanken als Referenz dient
  • Teile des Inhalts lassen sich auch auf CLI-Tools, Embedded-Firmware und Game Engines anwenden, diese sind aber nicht das optimierte Ziel
  • Als Hintergrundmaterial werden der Beitrag von 2017 “Go vs Rust? Choose Go.” und der Beitrag des Shuttle-Teams “Rust vs Go: A Hands-On Comparison” genannt
  • Go ist eine erfolgreiche Sprache, doch Designentscheidungen wie die breite Verwendung von nil, eine Fehlerbehandlung, die eher auf Disziplin als auf Typen setzt, und lange fehlende Generics sind im Vergleich zu Rust zentrale Streitpunkte
  • In der JetBrains Developer Ecosystem Survey wird Go als Sprache mit einem Anteil von 17–19 % aktiver Entwickler aufgeführt; Rust wächst zwar stetig, bleibt aber bei einem kleineren Anteil

Tooling

  • Sowohl Go als auch Rust verfügen über ein Batteries-included-Tooling, das Build, Test, Formatierung, Linting und Dependency-Management über konsistente Interfaces bereitstellt
  • cargo bietet als primäres Tool ein breiteres Spektrum an Funktionen, die den Go-Werkzeugen entsprechen
    • go.mod / go.sumCargo.toml / Cargo.lock: Projektkonfiguration und Dependency-Manifest
    • go get / go mod tidycargo add / cargo update: Dependencies hinzufügen und auflösen
    • go buildcargo build: Kompilieren
    • go run .cargo run: Nach dem Build ausführen
    • go test ./...cargo test: Tests
    • go vet ./...cargo clippy: Linter; Clippy ist dabei deutlich meinungsstärker als vet
    • gofmt / goimportscargo fmt: Auto-Formatter ohne Konfiguration
    • golangci-lint runcargo clippy -- -D warnings: Strikter Lint-Modus
    • go doccargo doc --open: API-Dokumentation erzeugen und öffnen
    • pprofcargo flamegraph / samply: CPU-Profiling
    • govulncheckcargo audit: Schwachstellenprüfung auf Basis einer Advisory-Datenbank
  • In Go werden Lücken oft mit Third-Party-Tools wie golangci-lint, mockgen, air oder goreleaser geschlossen, während Rusts primäres Ökosystem mehr Funktionen standardmäßig abdeckt
  • Selbst wenn externe Crates nötig sind, lassen sie sich mit einem einzigen cargo install cargo-nextest installieren und verhalten sich dann mit cargo nextest wie native Tools – ähnlich wie cargo watch oder cargo nextest
  • Der Vorteil von gofmt und rustfmt liegt weniger in Detailfragen des Stils als darin, Stil-Debatten in Code-Reviews zu eliminieren
    • Zitat aus Rob Pikes Go Proverbs: “Gofmt’s style is no one’s favorite, yet gofmt is everyone’s favorite.”

Zentrale Unterschiede zwischen Go und Rust

  • Beide Sprachen sind kompilierte, statisch typisierte Sprachen mit Deployment als einzelne Binärdatei und einem starken Nebenläufigkeitsmodell; der Unterschied liegt jedoch darin, was der Compiler garantiert und wie stark sich das Runtime-Verhalten kontrollieren lässt
  • Die wichtigsten Vergleichspunkte sind:
    • Stable Release: Go 2012, Rust 2015
    • Typsystem: Go ist statisch und strukturell typisiert und unterstützt seit 1.18 Generics; Rust ist statisch und nominal typisiert und unterstützt Generics, Traits und Lifetimes
    • Speicherverwaltung: Go nutzt nebenläufige Garbage Collection mit niedriger Latenz, Rust basiert auf Ownership und Borrowing und hat keine GC
    • Null-Sicherheit: In Go ist nil weit verbreitet, Rust kennt kein Null und nutzt Option<T> als Ersatz auf Typebene
    • Fehlerbehandlung: Go verwendet das error-Interface und if err != nil { ... }, Rust verwendet Result<T, E>, den ?-Operator und vollständiges Pattern Matching
    • Nebenläufigkeit: Go setzt auf CSP mit Goroutines und Channels, Rust auf async/await auf tokio sowie auf Channels und Threads
    • Abbruch/Cancelation: Go nutzt das konventionsbasierte context.Context, Rust explizite und typgeprüfte Weitergabe etwa mit CancellationToken
    • Data Races: Go erkennt sie probabilistisch zur Laufzeit mit -race, Rust erkennt sie zur Compile-Zeit mit Send/Sync
    • Compile-Zeit: Go ist sehr schnell, Rust ist vor allem bei Clean Builds langsam
    • Runtime: Go bringt eine Runtime von rund 2 MB und GC mit, Rust hat abgesehen von libc keine Runtime oder kann mit MUSL vollständig statisch gebaut werden
    • Größe des Ökosystems: Go etwa 750.000+ Module, Rust 250.000+ Crates
  • Prüfungen für nil-Behandlung, Fehlerweitergabe, Data Races, Ressourcenlebensdauer, Abbruch und Generics, die in Go auf Konventionen, Tools und Laufzeiterkennung beruhen, werden in Rust in das Typsystem verlagert
  • Rusts Mutex<T> erlaubt den Zugriff auf den inneren Wert nur über einen per .lock() erhaltenen Guard und entfernt damit den ganzen „Pfad, auf dem man vergisst zu locken“, bereits auf Typebene
  • Dasselbe Muster wiederholt sich bei Option, Result, &mut T, Send/Sync und RAII-Guards; mit wachsender Vertrautheit übernimmt der Compiler damit einen Teil der mentalen Checkliste

Die Grenzen von Go, die zu einer Prüfung von Rust führen

  • Da Go für die meisten Backend-Workloads ausreichend schnell ist, liegen die Hauptgründe für eine Prüfung von Rust eher in der Ausführlichkeit der Fehlerbehandlung, der Gefahr von nil-Zeigern und dem Fehlen ausgefeilter Features des Typsystems wie Enums und Traits als in der Geschwindigkeit
  • Go-Interfaces sind kein vollwertiger Ersatz für Rust-Traits, und da in der Standardbibliothek ein Set-Typ fehlt, sind idiomatische Umgehungen wie map[T]struct{} nötig
  • nil-Panics in der Produktion

    • Ein Go-Service kann monatelang normal laufen und dann auf einem bestimmten Codepfad in eine goroutine-Panic laufen, weil eine Prüfung auf einen nil-Zeiger übersehen wurde
    • Im Beispiel gibt Find (*User, error) zurück, und bei „not found“ ist error zwar nil, die Prüfung von user bleibt aber dem Aufrufer überlassen
    • user.Account.Notify() kann abstürzen, wenn user oder Account nil ist
    • Linter und IDE-Prüfungen wie nilaway und staticcheck erkennen einen Teil dieser Fälle, sind aber Opt-in, probabilistisch und funktionieren nicht zuverlässig über Paketgrenzen hinweg
    • Rusts Option<T> verhindert das Dereferenzieren, solange der Fall None nicht behandelt wurde, und beseitigt damit diese Kategorie von Ausfällen
  • Datenrennen, die -race nicht erfasst hat

    • go test -race ist ein hervorragendes Tool, aber als Laufzeit-Detektor findet es nur Datenrennen, die während der Tests tatsächlich ausgeführt wurden
    • In Go wird auch Code kompiliert, in dem zwei goroutines ohne Lock eine map verändern, und dieser kann dann unter Last in der Produktion scheitern
    • In Rust erfordert gemeinsam genutzter veränderbarer Zustand zwischen Threads Typen, die Send und Sync implementieren, und der Versuch, eine gewöhnliche HashMap zwischen Threads zu teilen, kompiliert nicht
    • Man wird dazu gezwungen, entweder Arc<Mutex<...>>, Arc<RwLock<...>> oder Channels zu verwenden, wodurch Race Conditions zu Typfehlern werden
    • Paul Dix nennt die Beseitigung von Datenrennen ausdrücklich als Grund für das Rewrite von 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."
      • Quelle: Paul Dix, Founder & CTO, InfluxData, Rust in Production
  • Komponierbare Fehlerbehandlung

    • Gos if err != nil { return err } kann die eigentliche Logik einer Funktion verwässern, und das Einbetten von Kontext mit fmt.Errorf("doing X: %w", err) hängt von Disziplin statt von Compiler-Regeln ab
    • Im Lobste.rs-Thread widersprechen erfahrene Go-Entwickler und argumentieren, dass errcheck und golangci-lint die meisten ausgelassenen Fehlerbehandlungen erkennen und explizites if err != nil lesbarer sei als dichte ?-Ketten
    • Peter Bourgon stellt die explizite Fehlerbehandlung in Go als bewusst intendierten kulturellen Wert dar
      • "I think that error handling should be explicit, this should be a core value of the language."
      • Quelle: Peter Bourgon, GoTime #91, zitiert in Dave Cheneys Zen of Go
    • Rusts Result<T, E> ist selbst Teil der Typsignatur und kann daher nicht vergessen werden; mit per thiserror::Error definierten Enums und #[from] erhält man Fehlerkonvertierung und Vollständigkeitsprüfungen
    • Fügt man eine neue Error-Variante hinzu, zeigt der Compiler an, welche match-Stellen aktualisiert werden müssen
  • Generics ohne Boxing

    • Die Generics in Go 1.18 sind nützlich, haben aber Einschränkungen wie das Fehlen von Methoden mit Typparametern, GC shape stenciling und mitunter überraschende Performance-Eigenschaften
    • Rust-Generics werden monomorphisiert, sodass jede Instanziierung spezialisierten Code erzeugt und keine Laufzeitkosten verursacht
    • In Kombination mit Traits sind Zero-Cost-Abstractions möglich
    • Das ist weniger wichtig für Handler-Code als für gemeinsam genutzte Infrastruktur wie Middleware, Generic Repositorys, Decoder und Parser; Go landet in solchen Bereichen oft wieder bei interface{}/any und Type Assertions
  • Vorhersehbare Latenz

    • Gos GC ist ausgezeichnet, nebenläufig und latenzarm und gut auf typische Service-Workloads abgestimmt, aber „low-pause“ ist nicht gleich „no-pause“
    • In Szenarien mit vielen Allokationen kann der P99-Latency-Tail schlechter ausfallen als bei einer Rust-Implementierung, die auf dem Hot Path nicht alloziert
    • In latenzsensitiven Systemen wie Trading, Real-Time-Bidding, Netzwerk-Proxys oder Ingestion mit hohem Durchsatz ist das Fehlen von GC-Pausen ein realer Vorteil
    • Stephen Blum sagt, dass Rust nötig sei, um bei der Größenordnung von PubNub die erforderliche Preis-pro-Dollar-Performance-Kapazität zu erreichen
      • "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."
      • Quelle: Stephen Blum, CTO, PubNub, Rust in Production

Rust-Entsprechungen zu Go-Mustern

  • Ein schneller Weg, sich an Rust zu gewöhnen, besteht darin, bereits bekannte Go-Muster auf die entsprechenden Rust-Muster abzubilden
  • Ein längeres Beispiel, das denselben Backend-Service in beiden Sprachen implementiert, findet sich im Shuttle comparison
  • Fehlerbehandlung: if err != nil vs. Result<T, E>

    • Go gibt nach os.ReadFile(path) und json.Unmarshal mit if err != nil einen Fehler zurück, der mit Kontext angereichert ist
    • Rust besteht aus fs::read_to_string(path)?, serde_json::from_str(&data)?, Ok(cfg)
    • Der Operator ? ersetzt das Muster if err != nil { return err } und übernimmt auch die Typumwandlung, wenn From<E1> for E2 implementiert ist
    • #[from] aus thiserror unterstützt diese Umwandlung idiomatisch
  • Null: nil vs. Option<T>

    • In Go gibt GetUser(id string) *User nil zurück, wenn kein Benutzer gefunden wird, und wenn der Aufrufer fmt.Println(u.Name) ausführt, führt das bei nil zu einer Panic
    • In Rust gibt get_user(id: &str) -> Option<User> entweder Some(User) oder None zurück
    • let user = get_user("123"); println!("{}", user.name); führt zu einem Compilerfehler, weil user kein User, sondern ein Option<User> ist
    • Mit match get_user("123") müssen sowohl Some(u) als auch None behandelt werden
    • In sicherem Rust gibt es kein nil, und Referenzen können nicht null sein
  • Interfaces vs. Traits

    • Go-Interfaces sind strukturell, und ein Typ erfüllt ein Interface implizit
    • Rust-Traits sind nominal und müssen explizit implementiert werden
    • Der Go-Ansatz eignet sich gut für spontanes Duck Typing, der Rust-Ansatz ist besser für Refactoring und Discoverability, und man kann per grep bestimmte Trait-Implementierungen finden
    • Generische Funktionen mit Trait Bound wie fn handle<R: Reader>(r: R) decken in den meisten Fällen alles ab, und dank Monomorphisierung gibt es kein Runtime-Dispatch
    • Um heterogene Implementierungen mit Runtime-Dispatch zu speichern, verwendet man Box<dyn Trait> oder Arc<dyn Trait>
  • Goroutine vs. Async-Task

    • Gos Nebenläufigkeitsmodell ist so einfach wie go doWork(ctx, input); Goroutinen sind leichtgewichtig, und die Runtime schedult sie über OS-Threads
    • Ein großer Vorteil von Go ist, dass es keinen syntaktischen Unterschied zwischen sequentiellem und parallelem Code gibt
    • Rust verwendet in Backend-Services fast immer async/await auf einem tokio-Executor
    • Async-Funktionen geben ein Future zurück und werden erst ausgeführt, wenn sie awaited oder gespawnt werden
    • Der Compiler verfolgt Send/Sync vor und nach .await-Punkten; wenn ein non-Send-Wert über ein await hinaus gehalten wird, entsteht ein Compilerfehler
    • Da es keine goroutine-artige eingebaute Präemption gibt, kann bei lang laufender CPU-gebundener Arbeit innerhalb einer Async-Task der Executor verhungern; solche Arbeit sollte an tokio::task::spawn_blocking oder rayon übergeben werden
  • context.Context vs. CancellationToken

    • In Go wird context.Context an alle blockierenden Aufrufe weitergegeben
    • Rust hat kein eingebautes context.Context; das nächstliegende Gegenstück für Abbruch ist tokio_util::sync::CancellationToken
    • Timeouts werden mit tokio::time::timeout(dur, fut) umgesetzt, indem das Future umhüllt wird
    • Deadlines und Werte werden statt über ein einzelnes Context-Objekt oft über explizite Argumente oder tracing-Spans weitergegeben
    • Zitat aus Dave Cheneys The Zen of Go:
      • „Go doesn’t have a way to tell a goroutine to exit. There is no stop or kill function, for good reason. If we cannot command a goroutine to stop, we must instead ask it, politely.”
    • In Go ist diese „höfliche Bitte“ üblicherweise das weitergereichte context.Context; in Rust sind es CancellationToken oder watch-Channels, aber der Compiler kann auf Auslassungen hinweisen
  • Strings: string vs. String und &str

    • Gos string ist ein UTF-8-Byte-Slice; bei Zuweisung wird der Header kopiert, während die zugrunde liegenden Bytes als unveränderliche Struktur gemeinsam genutzt werden
    • Rust teilt das in zwei Typen auf
      • String: besitzt die Daten, ist auf dem Heap alloziert und vergrößerbar
      • &str: eine geborgte Sicht auf andere String-Daten und entspricht in den meisten Fällen einem Go-string-Parameter
    • Die Faustregel lautet, für Argumente &str zu verwenden und beim Erzeugen neuer Daten String zurückzugeben
    • Die Trennung von &str und String zeigt in komprimierter Form Rusts Modell von „borrow vs own“

Bewertung von Go-Generics

  • Go führte Generics in Version 1.18 im März 2022 ein, also 13 Jahre nach dem Start der Sprache
  • Generics sind nützlich, bieten aber nicht in vollem Umfang die Vorteile, die man von Rust, Haskell oder modernem C++ erwartet, und bringen zugleich einen erheblichen Teil der Nachteile generischer Typsysteme mit sich
  • Die Standardbibliothek nutzt sie kaum

    • Auch drei Jahre nach der Einführung von Generics vermeidet die Go-Standardbibliothek sie größtenteils
    • sort.Slice nimmt weiterhin eine func(i, j int) bool-Closure statt eines cmp.Ordered-Constraint an
    • sync.Map ist weiterhin als any/any typisiert
    • Vorhandene generische Helper finden sich nur in wenigen Paketen wie slices, maps, cmp und einigen Einträgen unter sync
    • Das Go-1-Kompatibilitätsversprechen erklärt teilweise, warum sich bestehende nicht-generische APIs nur schwer umbauen lassen, aber anders als Rust nutzt Go Generics nicht als primäres Werkzeug
    • In Rust sind Generics von Anfang an in Option<T>, Result<T, E>, Vec<T>, HashMap<K, V>, Iterator, From/Into sowie in allen Collections und Smart Pointern verankert
  • Kein Trait-System, nur strukturelle Constraints

    • Rust-Generics sind mit Traits verknüpft, die ad-hoc-Polymorphismus, Supertraits, Associated Types, Blanket Impls und Coherence abdecken
    • Go-Constraints ähneln eher Interfaces, die um den ~-Operator für Type-Set-Membership ergänzt wurden
    • Go kennt keine Supertrait-Hierarchie wie in Rusts trait Ord: Eq + PartialOrd, keine Associated Types wie type Item; in Iterator und keine Blanket Impls wie impl<T: Display> ToString for T
    • In Go lassen sich Methoden mit Typparametern nicht verwenden, daher sind Formen wie func (s Set[T]) Map[U](<https://corrode.dev/learn/migration-guides/go-to-rust/f func(T>) U) Set[U] nicht möglich
    • Sobald eine Abstraktion über „eine Funktion, die für ein beliebiges T mit einigen Operationen funktioniert“ hinausgeht, fällt Go wieder auf any, Type Assertions, Codegenerierung und Runtime-Reflection zurück
  • Unterschiede bei Typinferenz und Implementierungsstrategie

    • Rust propagiert Typinformationen über ganze Ausdrücke hinweg, einschließlich Closures, Iterator-Chains und dem ?-Operator
    • Die Inferenz in Go ist flacher und leitet Typparameter meist aus Funktionsargumenten ab, kann sie aber nicht aus dem Rückgabekontext ableiten, weshalb an der Aufrufstelle oft explizite Type Arguments nötig sind
    • Go wählt mit GCShape stenciling and dictionaries einen Mittelweg, um schnelle Compile-Zeiten zu behalten, dafür kann bei jedem Methodenaufruf auf Typparametern eine Indirektion entstehen
    • Als Beleg dafür wird der PlanetScale-Artikel angeführt
    • Rust erzeugt dagegen jeweils spezialisierten Maschinencode für Vec<i32> und Vec<String> und benötigt kein Runtime-Dispatch
    • Der Preis der Monomorphisierung ist die Compile-Zeit; beide Sprachen optimieren auf unterschiedliche Ziele hin
  • Schließt die Lücken des Typsystems nicht

    • In Rust beseitigen Generics und Traits die meisten Situationen, in denen sonst Box<dyn Any> oder Runtime-Reflection nötig wären
    • Go-Generics schaffen es nicht, any, reflect oder die dominanten Codegenerierungsmuster bei ORMs, Decodern und Mocks zu verdrängen
    • encoding/json nutzt weiterhin Reflection, database/sql weiterhin any, und mockgen erzeugt weiterhin Code
    • Go-Generics wirken wie ein neues Werkzeug, das in engen Fällen nützlich ist, während Rust-Generics wie ein Fundament funktionieren, ohne das die Sprache zusammenbrechen würde

Rust-Backend-Ökosystem

  • Auch im Rust-Ökosystem gibt es für allgemeine Backend-Services inzwischen eine gewisse Konvergenz bei den „Standardoptionen“
  • Typische Entsprechungen:
    • HTTP-Server: Go net/http, chi, gin, echo, fiber → Rust axum auf hyper
    • HTTP-Client: Go net/http, resty → Rust reqwest
    • gRPC: Go google.golang.org/grpc + protoc-gen-go → Rust tonic + prost
    • SQL: Go database/sql, sqlc, sqlx, gorm → Rust sqlx, sea-orm, diesel
    • Migrationen: Go golang-migrate, goose → Rust sqlx migrate, refinery
    • JSON: Go encoding/json, sonic, goccy/go-json → Rust serde + serde_json
    • Logging: Go log/slog, zerolog, zap → Rust tracing + tracing-subscriber
    • Metriken: Go prometheus/client_golang → Rust metrics + metrics-exporter-prometheus
    • Konfiguration: Go viper, koanf → Rust config / config-rs, figment
    • CLI: Go cobra, urfave/cli → Rust clap derive
    • Fehler: Go errors, pkg/errors → Rust thiserror für Bibliotheken, anyhow für Binärprogramme
    • Testen: Go testing, testify, gomega → Rust eingebautes #[test], rstest, assert_matches
    • Mocking: Go mockgen, moq → In Rust sind handgeschriebene Fakes idiomatisch, mockall wird ebenfalls verwendet
    • Hintergrund-Tasks: Go-Goroutines + errgroup → Rust tokio::spawn + JoinSet
  • Für einen typischen Backend-Service deckt die Kombination axum + sqlx + tokio + tracing + serde + clap laut Darstellung 90 % dessen ab, was benötigt wird

Borrow Checker und Lernkurve

  • Beim Wechsel von Go zu Rust sollte man davon ausgehen, dass man gegen eine Wand laufen wird
  • Die Go-Runtime kümmert sich um Speicher und Aliasing selbst, Rust verlagert diese Entscheidungen dagegen ins Typsystem, weshalb in den ersten Wochen Code, der „eigentlich funktionieren müsste“, vom Compiler abgelehnt werden kann
  • Muster, auf die Go-Entwickler häufig stoßen:
    • Langlebige Referenzen: In Go ist es selbstverständlich, ein *User, das aus einer Map geholt wurde, lange zu halten, in Rust verhindert diese Ausleihe jedoch Änderungen an der Map, solange sie lebt
    • Selbstreferenzielle Structs: In Go kann man Daten und einen Iterator über diese Daten in derselben Struct unterbringen, in Rust braucht man dafür Pin, ouroboros oder ein Redesign
    • Gemeinsamer veränderbarer Zustand zwischen Goroutines: Das Go-Muster mu sync.Mutex; data map[K]V wird in Rust zu Arc<Mutex<HashMap<K, V>>>
    • Referenzen aus Funktionen zurückgeben: Lifetime-Annotationen kommen ins Spiel, ein neues Konzept für Go-Entwickler
  • Der Borrow Checker sollte nicht als störender „Torwächter“ gesehen werden, sondern als Mechanismus, der real existierende Bugs aufdeckt
  • Er filtert bereits zur Compile-Zeit Fälle heraus, in denen Werte nach einem Move erneut verwendet werden, mehrere Threads gleichzeitig dieselben Daten anfassen, Null- oder Dangling-Pointer dereferenziert werden oder Referenzen länger leben als die Werte selbst
  • Wenn man das Borrowing-Konzept verinnerlicht, wird der Borrow Checker vom Gegner zum Verbündeten, und erfahrene Rust-Entwickler sagen meist, dass er innerhalb von 4 bis 12 Wochen zu einem Helfer geworden ist
  • PubNub-CTO Stephen Blum beschrieb den ersten Monat bei Rustacean Station als „wie damals, als ich zum ersten Mal Programmieren lernte“, weil er sich zwangsläufig mit Borrow Checker und Lifetimes auseinandersetzen musste
  • clap-Maintainer Ed Page sagte bei Rustacean Station: clap with Ed Page, der Borrow Checker habe ihm geholfen, sich auf Probleme höherer Ebene zu konzentrieren, und auch Dinge gefunden, die seiner eigenen Analyse entgangen waren

Zentrale Hürden beim Umstieg auf Rust

  • Compile-Zeit

    • Rust-Compile-Zeiten muss man im Vergleich zu Go klar als Rückschritt sehen; ein sauberer Release-Build eines mittelgroßen Services kann Minuten dauern, während Go fast sofort kompiliert
    • Incremental Builds und cargo check sind praktikabel, und die Compile-Zeiten haben sich über die Jahre verbessert, aber der Unterschied zu Go ist deutlich spürbar
    • Für die Editier-Schleife sollte man cargo check nutzen, bei erkennbarem Nutzen in Workspaces aufteilen und Crates mit vielen prozeduralen Makros in separaten Crates halten, damit sie nur bei Änderungen neu kompiliert werden
    • Mehr dazu findet sich in Tipps zum Verkürzen von Rust-Compile-Zeiten
  • Async-Coloring-Problem

    • Die Trennung zwischen async fn und fn in Rust ist beim Wechsel von Go eine der größten Usability-Verschlechterungen
    • Async Trait ist seit Rust 1.75 stabil, beim Zusammenspiel mit dynamischem Dispatch gibt es aber weiterhin raue Kanten
    • In manchen Situationen greift man zum Crate async-trait, um diese Probleme zu kaschieren
  • Kleineres Ökosystem

    • Das Rust-Crate-Ökosystem wächst und die Qualität der Bibliotheken ist insgesamt hoch, aber Go liegt in einigen backendnahen Bereichen vorn
    • Bereiche, in denen Go voraus ist, umfassen Kubernetes-Operatoren, SDKs von Cloud-Anbietern und Datenbanktreiber für bestimmte Nischen-Storage-Systeme
    • Bevor man die Migration festzurrt, sollte man sich etwa einen Tag Zeit nehmen, um zu prüfen, ob es für die benötigten Bibliotheken brauchbare Rust-Alternativen gibt
    • Manche Teams müssen verwaiste Crates zur XML-Schema-Validierung aktualisieren oder Clients für weniger verbreitete Protokolle selbst schreiben

Integrationsstrategien

  • Ein erfolgreicher Wechsel von Go zu Rust ist eher eine taktische Entscheidung als ein komplettes Rewrite in einem Zug
  • Microsoft Principal Engineer Victor Ciura sagte in Rust in Production: „Es geht nicht darum, alles zum Spaß in Rust neu zu schreiben, sondern um eine taktische Entscheidung, Rust dort einzusetzen, wo neue Komponenten besser dazu passen.“
  • 1. Hot Path als Service herauslösen

    • Wenn ein bestimmter Service wiederholt Probleme macht, ist es die risikoärmste Migration, genau diesen Service hinter demselben API-Vertrag in Rust neu zu schreiben
    • Das Ziel kann ein Service mit hoher CPU-Auslastung, Latenzempfindlichkeit oder wiederkehrenden Stabilitätsproblemen sein
    • Andere Go-Services kommunizieren weiter per HTTP/gRPC und müssen die interne Implementierungssprache nicht kennen
    • Radar-CTO Jeff Kao sagte in Rust in Production, dass Discords Beitrag zum Wechsel von Go zu Rust Radar auf dieselbe Idee gebracht habe
  • 2. Sidecar- oder Worker-Prozesse ersetzen

    • Hintergrund-Worker, Queue-Consumer, Ingestion-Pipelines und CPU-gebundene Batch-Jobs sind gute erste Kandidaten
    • Sie haben in der Regel klare Ein-/Ausgabegrenzen wie Queues oder Topics und keinen In-Process-Shared-State mit dem Rest des Systems
  • 3. cgo ist möglich, aber schmerzhaft

    • Aus Go heraus kann man Rust über cgo aufrufen, und es gibt auch eine gute Anleitung dazu
    • Für Backend-Services wird das meist nicht empfohlen
    • Build-Komplexität und FFI-Overhead heben die Vorteile gegenüber dem Ansatz „einen Rust-Service aufsetzen und hinter einen Netzwerkaufruf stellen“ oft auf
    • Für Bibliotheken und CLI-Tools kann es praktikabler sein
  • 4. Strangler Pattern hinter einem Gateway anwenden

    • Mit einem API-Gateway oder Reverse Proxy kann man nur bestimmte Endpoints an den neuen Rust-Service weiterleiten und den Rest in Go belassen
    • Das passt besonders gut, wenn ein abgegrenzter Bounded Context wie Authentifizierung, Suche oder Bezahlung als Migrationseinheit taugt
    • Dieses Muster heißt „strangler fig“, weil der neue Service um den bestehenden herumwächst und ihn am Ende vollständig ersetzt

Praxistipps für die Migration

  • Man sollte mit Services mit klaren Grenzen beginnen und nicht den zentralsten oder am häufigsten deployten Service wählen
  • Wähle einen Service, dessen Vertrag zum Rest des Systems gut definiert ist und dessen Auswirkungsradius klein bleibt
  • Denselben API-Vertrag beibehalten

    • Wenn der Go-Service eine REST-API bereitstellt, sollte der Rust-Service dieselben Pfade, dieselben JSON-Formen und dieselben Error-Wrapper beibehalten
    • Für Clients bleibt die Migration unsichtbar, und per Gateway kann Traffic schrittweise umgestellt werden
  • Idiome nicht wörtlich übertragen

    • if err != nil { return err } wird zu ?
    • Das Muster „eine Goroutine pro Request“ wird nur dann mit tokio::spawn übernommen, wenn es wirklich nötig ist
    • axum verarbeitet Requests bereits parallel
    • Interfaces mit nur einer Methode werden meist zu Trait Bounds in Generics statt zu Box<dyn Trait>
  • Den Compiler wie einen Pair-Programmer nutzen

    • Rust-Compiler-Fehlermeldungen sind in der Regel hochwertig, und wenn man sie langsam liest, zeigen sie fast immer die richtige Lösung
    • Teammitglieder, die am längsten kämpfen, sind meist diejenigen, die den Compiler nicht als Partner sehen, sondern gegen ihn arbeiten
  • Früh in Training investieren

    • Wenn man versucht, die Rust-Migration nebenbei zu lernen, endet das oft nicht gut
    • Man sollte sich tatsächlich Lernzeit nehmen, etwa für Workshops, Online-Kurse oder Pairing-Sessions an echter Codebasis
    • Sobald das Team sicherer wird, zahlt sich diese Vorabinvestition mehrfach aus

Bereiche, in denen Go weiterhin passend ist

  • Es ist nicht nötig, alles nach Rust zu migrieren, und es gibt Bereiche, in denen Go besonders gut geeignet ist
  • Kubernetes-native Tools

    • Bei Operatoren, Controllern und CRDs ist das Ökosystem überwältigend stark auf Go ausgerichtet
  • CLI-Utilities und Entwickler-Tools

    • Schnelle Kompilierung, einfaches Cross-Compiling und unkomplizierte Bereitstellung sind klare Stärken
  • Glue-Services

    • Bei dünnen API-Schichten, Proxys und Formatkonvertern lohnt sich der Boilerplate-Anteil von Rust möglicherweise nicht
  • Wo Team-Geschwindigkeit wichtiger ist als absolute Korrektheitsgarantien

    • In Bereichen, in denen man sich schnell bewegen muss, kann Go weiterhin passend sein
    • Canonical VP of Engineering Jon Seager sagt in Rust in Production, dass Go eine sehr gute Wahl für Netzwerkdienste sei, dass es bei Canonical viel Go gebe und dass Juju ebenfalls eine riesige Go-Codebasis sei
    • Eine Hybridstrategie ist üblich, und viele Teams landen bei einem mehrsprachigen Backend: Go für „langweilige“ Services, Rust für Services, bei denen zusätzliche Stabilität und Performance den Mehraufwand rechtfertigen

Zu erwartende Verbesserungen

  • Die Zahlen variieren je nach Workload stark und sollten daher als grobe Orientierung, nicht als Versprechen verstanden werden
  • Ungefähre beobachtete Verbesserungsbereiche bei Migrationen von Go nach Rust:
    • CPU-Auslastung: 20–60 % weniger
      • Da Go bereits effizient ist, fällt der Effekt meist weniger dramatisch aus als bei einer Migration von Python nach Rust
      • Vorteile entstehen durch das Fehlen von GC und durch engere Loops
    • Arbeitsspeicher: 30–50 % weniger
      • Hauptgründe sind der Wegfall des GC-Overheads und eine kleinere Runtime
    • P99-Latenz: deutlich konsistenter
      • Rust-Services zeigen tendenziell weniger GC-bedingten Jitter und flachere Latenzspitzen als Go-Services
      • Seit der Einführung der Low-Latency-GC hat sich auch Go stark verbessert, aber unter hoher Last bleibt ein Unterschied
    • Produktionsstörungen: der Bereich, in dem Teams die Verbesserungen am häufigsten hervorheben
      • Fehlerarten wie Data Races, nil-Dereferenzierungen oder fehlende Fehlerpfade, die go test -race bestehen und trotzdem in Produktion gelangen, kompilieren in Rust nicht
      • Nach einer Rust-Migration werden On-Call-Rotationen in der Regel sehr langweilig
  • InfluxData Staff Engineer Andrew Lamb sagt in Rustacean Station: Rebuilding InfluxDB with Rust, dass nach dem Rewrite von InfluxDB Abstürze, seltsame Multithreading-Race-Conditions und das Nachverfolgen zuvor sehr zeitaufwendiger Probleme wegfielen
  • Wer von Go nach Rust wechselt, wird die 10-fache Durchsatzsteigerung, wie sie bei einem Wechsel von Python nach Rust möglich sein kann, eher nicht sehen
  • Der eigentliche Nutzen liegt in weniger „absurden Fehlern“, flacheren Latenz-Tails und der Fähigkeit, mit derselben Sprache auch in andere Bereiche wie Embedded-Entwicklung oder Systemprogrammierung vorzudringen

Zusätzliche Hinweise

  • Das Typsystem von Rust beseitigt nicht jeden Fehler in der Synchronisationslogik, aber Typen, die nicht ohne Synchronisation zwischen Threads geteilt werden dürfen, kompilieren nicht
  • Die Art von Problemen, bei denen „vergessen, zu locken“ zu stiller Datenkorruption führt, kann das Typsystem von Rust verhindern
  • Go-string ist eine unveränderliche Byte-Sequenz und konventionsgemäß UTF-8, aber nicht auf Typebene garantiert
  • Die nächstliegenden Entsprechungen sind Go-string ↔ Rust-&str für schreibgeschützte Sichten und Go-[]byte ↔ Rust-Vec<u8> für veränderliche Buffer
  • Rust-String ist die besitzende, erweiterbare Version von &str und bietet zusätzlich die Garantie, dass der Inhalt gültiges UTF-8 ist
  • Mehr dazu findet sich in Strings, bytes, runes and characters in Go
  • Seit Go 1.18 sind generische Funktionen und generische Typen möglich, Typ-Parameter direkt an Methoden wurden jedoch nicht eingeführt
  • Iterator-Ketten wie in Rusts (0..100).filter(|n| ...).collect() können auf Go-Entwickler ungewohnt wirken, aber auch in Rust lassen sich for-Loops verwenden, und für einmaligen Code sind sie oft die richtige Wahl

Fazit

  • Der Wechsel von Go zu Rust unterscheidet sich vom Wechsel von Python oder TypeScript nach Rust
  • Entwickler mit Go-Hintergrund kennen die Vorteile statischer Typisierung und kompilierter Sprachen bereits, es geht also nicht darum, dynamische Typen oder eine langsame Runtime aufzugeben
  • Der zentrale Tausch besteht darin, nil hinter sich zu lassen und dafür eine robustere Codebasis, weniger Fallstricke und einen strengeren Compiler zu bekommen, der mehr Fehler schon zur Compile-Zeit findet
  • Dafür ist die Lernkurve steiler
  • Bei Diensten, von denen eine Organisation abhängt, die hohe Verfügbarkeit benötigen und geschäftskritisch sind, wie grundlegende Services, ist dieser Tausch eindeutig wertvoll
  • Bei anderen Services kann Go weiterhin die richtige Antwort sein
  • Das Ziel einer Migration ist, jede Aufgabe in der Sprache zu platzieren, die das jeweilige Problem am besten löst

1 Kommentare

 
GN⁺ 3 시간 전
Hacker-News-Kommentare
  • Ein Wechsel von C/C++ oder Python zu Rust ist aus mehreren Gründen nachvollziehbar, aber für ein Web-Backend scheint Go eine gute Wahl zu sein
    Ich verwende fast nur Rust, aber als ich zuletzt an einem Webserver in Rust gearbeitet habe, dachte ich, ich hätte lieber Go nehmen sollen
    Im Original wird angemerkt, dass Gos Syntax für Fehlerbehandlung umständlich ist, und das stimmt. Rust hatte dasselbe Problem und hat dann die ?-Syntax ergänzt, die bei einem Fehler den Fehlerwert zurückgibt. Gos Fehlerbehandlung ist meistens die ausgeschriebene Form davon
    Rust hat keinen vereinheitlichten Fehlertyp, sondern mehrere wichtige Fehler-Ansätze wie io::Error, thiserror und anyhow, was beim Weiterreichen entlang der Aufrufkette nach oben lästig ist
    Es gibt Dinge, die in einer neuen Sprache fehlen können und sich später nur schwer nachrüsten lassen. Dazu gehören Konstantentypen, Boolesche Typen, Fehlertypen, mehrdimensionale Array-Typen sowie Vektor-/Matrix-Typen der Größe 2/3/4 und Standardoperationen darauf. Wenn man das nicht früh standardisiert, verbringt man viel Zeit damit, mehrere Darstellungen desselben Konzepts aufeinander abzustimmen
    Abgesehen von der Fehlerbehandlung wirkt sich das bei Webentwicklung weniger aus, aber bei numerischen Berechnungen, Grafik und Modellierung ist es sehr schmerzhaft, weil man Standardoperationen auf Zahlenarrays anwenden können muss
    Go hat bei Webservices zwei Vorteile. Der erste sind die im Original erwähnten Goroutinen, der zweite sind die im Original weniger behandelten Bibliotheken. Go hat die meisten Bibliotheken, die man für Webservices braucht, und viele davon werden auch intern bei Google verwendet und haben daher sehr harte Umgebungen überstanden. Rust-Crates sind dagegen oft weniger ausgereift und haben keine offizielle Qualitätssicherung

    • Ich denke, der größte Vorteil von Go gegenüber Rust ist die Kompilierungsgeschwindigkeit
      Außerdem hängt Rust im Vergleich zu Go noch immer stark von vielen C/C++-Bibliotheken ab, wodurch Cross-Compiling, reproduzierbare Builds und die Erstellung statischer Binärdateien leicht problematisch werden
      Der Nachteil von Go ist, dass der Garbage Collector zu simpel ist. Wenn Latenzspitzen auftreten, gibt es außer einem schmerzhaften Rewrite kaum Gegenmaßnahmen
    • Rust hat praktisch genau einen Fehlermechanismus, nämlich das Error trait
      Die aufgezählten Dinge sind nur gängige Nutzungsweisen davon, und selbst nur mit Box gibt es überhaupt kein Problem. Das ist im Wesentlichen ähnlich zu dem, was anyhow::Error macht
    • Ich mochte Go eine Zeit lang ziemlich gern, aber seit ich zuletzt mehr Swift und Rust nutze, wirkt ein Compiler, der Null-Pointer-Dereferenzierung nicht verhindert und keine Garantien für nebenläufige Sicherheit gibt, etwas prähistorisch
      Bei der Standardbibliothek hat Go es meiner Meinung nach aber deutlich besser gemacht als Rust
    • Stimme zu. Mir ist am Anfang aufgefallen, dass der Artikel als etwas für Backend-Services beschrieben wurde
      Ich mag die Sprache Rust und nutze sie für Embedded-Firmware und PC-Anwendungen, aber für Web-Backends verwende ich immer noch Python. Rust hat einfach kein Toolset auf Django- oder Rails-Niveau
      Es gibt Dinge ähnlich wie Flask, aber nicht das robuste Flask-Ökosystem. Ich habe wenig Go-Erfahrung, aber für ein Web-Backend würde ich vermutlich Go statt Rust wählen. Der Grund ist das Ökosystem aus Bibliotheken und Frameworks
      Außerdem mag ich aus den üblichen Gründen Async Rust nicht besonders. Im Rust-Web-Ökosystem ist asynchrone Nutzung fast überall praktisch Pflicht
    • Rust hat nicht drei Fehlersysteme, sondern eines, nämlich das Error trait
      io::Error ist nur einer von vielen Typen, die es implementieren, und nichts Besonderes. Mit thiserror definierte Fehler implementieren dieses Trait ebenfalls
      anyhow dient nur dazu, bequem „irgendein Error“ sagen zu können, wenn man den Fehlertyp, den eine Funktion ausgeben kann, nicht detailliert als API-Vertrag ausschreiben will
  • Rust macht es leichter als Go, Code deterministisch zu halten, was sehr nützlich ist, wenn man deterministische Simulationstests und eigenschaftsbasierte Tests braucht
    Ich habe kürzlich mit Go ein Postgres-to-Iceberg-Datenspiegelungs-Tool geschrieben: https://github.com/polynya-dev/pg2iceberg
    Ich habe es aber nach Rust portiert, weil ich deterministische Simulationstests machen wollte, ohne gegen die Go-Runtime kämpfen zu müssen
    Wenn die betreffende Domäne diese Art von Tests allerdings nicht rechtfertigt, würde ich jederzeit Go statt Rust wählen
    Verwandter Artikel: https://www.polarsignals.com/blog/posts/2024/05/28/mostly-ds...

  • Es klingt vielleicht offensichtlich und wiederholt, aber mein größter Kritikpunkt an Rust ist die Lage beim Paketmanagement, und ich halte das vollständig für das Ergebnis der Denkweise der Entwickler
    Die Usability auf Rust-Seite gefällt mir. Der funktionale Ansatz bei Datentypen ist schön. Aber ich arbeite gerade parallel an einem Rust-Projekt und einem Go-Projekt, und die Abhängigkeitsbäume sind völlig unterschiedliche Tiere
    Das Go-Projekt kommt größtenteils mit der Standardbibliothek aus, aber im Rust-Projekt habe ich nur rusqlite (sqlite), clap (CLI), ratatui (TUI) und tauri (GUI) angefordert, und trotzdem scheint es über 400 Abhängigkeiten zu geben. Vor allem tauri ist der mit Abstand größte Verursacher, aber selbst ohne das sind es fast 100, was sich verrückt anfühlt
    Wenn es gut gepflegte Rust-Crate-Alternativen gäbe, die vernünftig mit Abhängigkeiten umgehen, wäre das deutlich besser, aber ich habe sie noch nicht gefunden. Ich will mir einfach keinen Shai-Hulud ins System holen, aber die Rust-Web-Leute scheinen in dieser Hinsicht cargo zu npm machen zu wollen

    • Man muss bedenken, dass viele Rust-Bibliotheken in mehrere Crates aufgeteilt sind und alle in den Abhängigkeitsgraphen eingehen
      Dadurch wirkt die Anzahl der Abhängigkeiten größer, als sie tatsächlich ist. Oft haben getrennte Crates denselben Maintainer und sind Teil desselben Upstream-Git-Repositories
      Trotzdem stimme ich dem Gesamteindruck zu. In Rust gibt es viele halb verlassene Crates in Version 0.x, und oft gibt es keine bessere Alternative
    • Ich finde, die Standardbibliothek ist der Ort, an den gute Ideen zum Sterben gehen
      Danach bekommt man dann httplib3 und anschließend httplib4
      Anders gesagt: Ich bevorzuge den Rust-Ansatz deutlich. Ob ich von der Standardbibliothek oder einer anderen Abhängigkeit abhänge, macht für mich keinen großen Unterschied. Es ist so oder so eine Abhängigkeit
      Dass etwas Teil der Standardbibliothek ist, bedeutet nicht automatisch, dass es qualitativ besser oder besser gepflegt ist; das sind getrennte Fragen
      Am Ende hängt alles von den Ressourcen ab. Natürlich kann eine Standardbibliothek mehr Ressourcen bekommen, aber sie kann genauso gut aufblähen und unwartbar werden
    • Ich frage mich, ob es außer vielleicht Java überhaupt eine Sprache gibt, deren Standardbibliothek all die Entsprechungen zu rusqlite, clap, ratatui und tauri enthält
      Außerdem besteht Tauri selbst aus 14 Crates, und jede davon taucht im Build-Tree auf
      https://github.com/tauri-apps/tauri/blob/dev/Cargo.toml
      Ratatui besteht ebenfalls aus 6
      https://github.com/ratatui/ratatui/blob/main/Cargo.toml
    • Paketmanagement ist bei fast allen Sprachen und Technologien ein Ärgernis
      Niemand hat es „gelöst“, und ich glaube auch nicht, dass es künftig eine einzelne Lösung geben wird
      Bei Go muss man darauf vertrauen, dass Bibliotheksentwickler Semantic Versioning korrekt einhalten, und man kann Versionen nicht pinnen. Das finde ich persönlich ziemlich störend
      Es gibt ein paar Workarounds. Man kann SHAs wie Git-Commit-Hashes verwenden, um so etwas wie Versionen zu erzeugen, oder Vendoring als bekannten Abhängigkeits-Cache nutzen. Vendoring bringt allerdings eigene Cache-Management-Probleme mit sich
      Ich musste am Wochenende mit Python-Virtual-Environments arbeiten, und das endete nicht gut; es erinnerte mich wieder daran, warum ich Python verlassen habe
      Perl mit CPAN, Java mit Maven/Gradle, Ruby mit Gems, Go mit dep/glide/vgo/modules, Rust mit Cargo, Node mit npm/yarn — alle haben ähnliche Probleme
      Bei Betriebssystemen ist es Redhat mit yum/rpm, Debian mit apt, Ubuntu mit snap und so weiter. Besonders bei snap frage ich mich wirklich, was das soll
    • Ich kenne mich mit Go nicht gut aus, aber ich frage mich, was in Gos Standardbibliothek dem Tauri-Äquivalent entsprechen soll
      Für den genannten Anwendungsfall könnte es vielleicht Sinn ergeben, das Frontend weiter in Go zu lassen und nur das Backend in Rust zu machen
  • Dieses Dokument fühlt sich seltsam an, weil es zugleich ein Migrationsleitfaden und ein Rust-Plädoyer sein will
    Wenn man am Ende überlegt, ob man Rust oder Go einsetzen soll, läuft die Kernfrage fast vollständig darauf hinaus: „Will man eine Managed Runtime oder nicht?“ Eine Generation von Rust-Programmierern hat sich eingeredet, dass Managed Runtimes schlecht seien und dass ihr Fehlen ein wichtiges Feature sei
    Das ist aber offensichtlich falsch. Es gibt mehr Programmierdomänen, die eine Managed Runtime wollen, als solche, die keine wollen
    Das bedeutet nicht, dass Go in all diesen Fällen die Standardwahl sein sollte. Es gibt viele subjektive Gründe, Rust zu bevorzugen. Wenn ich Go benutze, vermisse ich match, aber tokio und Async Rust vermisse ich nicht
    Beide sind in fast allen Fällen legitime Optionen, in denen man den Problemraum nicht gewaltsam verbiegen muss. Ein Linux-Kernel-Modul in Go zu schreiben, wäre zum Beispiel eine seltsame Wahl
    Der Rust-gegen-Go-Kampf ist ein merkwürdiger und peinlicher Seitenarm unseres Fachs. Ein großer Teil der Branche baut ganze Systeme sehr erfolgreich mit Python oder Node und lacht über die Nerds, die sich darüber streiten, welche statisch typisierte kompilierte Sprache sie verwenden sollen. Die eigentliche Frage ist Python gegen Rust/Go, nicht Rust gegen Go

    • Node zusammen mit PureScript zu verwenden, könnte ganz okay sein
      Insgesamt sollten Rust und Go aber ihre Kräfte gegen die Übel des dynamischen Tippens bündeln. Wenn Type Hints inzwischen als Best Practice gelten, ist das doch im Grunde ein Eingeständnis, dass da vorher ein Defekt war
      Selbst gute Type Hints sind schlechter als Typinferenz. Typinferenz erlaubt es, bei Typänderungen viel Code unverändert zu lassen und verhindert trotzdem unbeabsichtigte Typänderungen
    • In der Node-Welt hat man TypeScript übernommen, weil man statisch kompilierte Typen wollte
      Ich wünschte, TS hätte etwas mehr Runtime. Das Einzige, worauf ich bei Python neidisch bin, ist, wie natürlich sich dort JSON-Schema-Validierung an HTTP-Endpunkten anfühlt
      Das Prozedere mit Zod ist für mich ständig eine Quelle des Ärgers, und ich halte das für ein Problem, das durch die dogmatische Haltung des TS-Teams entstanden ist
  • Die Spuren von LLM-Schreibstil werden immer subtiler, sind aber noch immer sehr deutlich zu sehen. Besonders beim Wort genuine
    Etwa in Sätzen wie „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“ oder „There are cases where the borrow checker is genuinely too strict“
    Ich glaube nicht, dass der ganze Text KI-generiert ist, aber er wirkt KI-unterstützt. Falls das so ist, hat der Autor es genuinely gut gemacht
    Dass andere das nicht ansprechen, deutet darauf hin, dass es dem Inhalt nicht massiv schadet, aber es fühlt sich seltsam an, dass so etwas immer häufiger und schwerer erkennbar wird

    • Stimme zu, aber ich weiß nicht genau, warum. Ich kann nicht präzise benennen, was etwas nach KI-generiert klingen lässt
      Spätestens bei „Go is clearly working for a lot of people,“ begann ich KI-Unterstützung zu vermuten. Natürlich muss das nicht stimmen; ich bin nicht besonders gut darin, das zu erkennen
      Ironischerweise ist es weniger ein konkreter Hinweis als eher ein Gefühl. Wenn sich ein Text nach KI-Unterstützung „anhört“, verliere ich sofort das Interesse, auch wenn der Text an sich okay ist
      Ich wünschte, die Leute würden sich wieder wohler damit fühlen, ihre eigenen Gedanken direkt so aufzuschreiben, wie sie ihnen kommen
    • Völlig off-topic, aber it's worth being precise about ... ist ein noch viel stärker KI-artiger Ausdruck als die genuine-Verwendung
    • Ich denke, der ganze Text ist KI-generiert. Der Autor könnte einen Entwurf als Input gegeben und Teile der Ausgabe überarbeitet haben
      Dieses Absatzbeispiel etwa wirkt so: „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.”
      Jeder Satz sagt etwas, jeder Satz ist wichtig und erfüllt seine Aufgabe. So etwas erwartet man eher in einem sehr fachlichen Buch oder Paper als in einem Blogpost
      Gerade dadurch wird der Text schwerer lesbar und langweiliger
    • Im vergangenen Jahr ist mir aufgefallen, dass LLM-Schreiben eine ungewöhnlich starke Tendenz hat, über die Oberfläche und besonders über die Unterseite zu sprechen
      Ich erwarte nicht, dass LLM-generierter Text frei von Gemeinplätzen ist. Ich hoffe nur, dass wir alle wieder ein besseres Gespür fürs Redigieren bekommen, damit wir nicht ständig dieselbe Stimme lesen
  • Bei einem neuen Projekt kann man es ruhig in Rust schreiben
    Wenn aber schon bestehender Code da ist, das System funktioniert und Geld verdient, sollte man nur die Teile neu schreiben, die wirklich neu geschrieben werden müssen, und sonst in der ursprünglichen Sprache weitermachen
    Verbessere das System in kleinen, messbaren Schritten mit einer Sprache, die du kennst, und einem Team, dem du vertrauen kannst. Alles andere ist verschwenderischer Religionskrieg

    • Wenn ein Team mit C#/Java/Go usw. erfolgreich ausgeliefert hat und gut damit zurechtkommt, sehe ich keinen Grund, Rust zu verwenden
  • Ich mochte Rust schon vor Benchmarks, aber bei den meisten LLMs war der Unterschied in der Effizienz, Rust und Go zu schreiben, viel größer als ich gedacht hatte. Besonders bei agentischen Harnesses, die anfängliche Umgebungsprobleme beheben können
    Das hat mich ziemlich stark zu einem Rust-Evangelisten gemacht. Ich habe gute Ergebnisse erzielt, indem ich Batch-Verarbeitungs-Tools in Rust geschrieben habe, die aus bestehendem Code aufgerufen werden, aber eine vollständige Produktionsmigration habe ich noch nicht versucht
    Die im Text genannten Probleme von Go, insbesondere rund um nil, scheinen sich durch sehr gründliche Code-Reviews mit Codex zunehmend abfedern zu lassen. Noch besser wäre es natürlich, wenn das Problem gar nicht erst existierte, aber für Entwickler, die in Review und Verständnis ähnlich viel Mühe investieren wie in Design und Implementierung, werden solche Sicherheitsbugs zunehmend optional
    Die Sprachdaten stehen hier: https://gertlabs.com/rankings?mode=agentic_coding

    • Dank detaillierter Compiler-Fehler und eines starken Typsystems können Agenten den Zyklus korrigieren → kompilieren → korrigieren gut handhaben
      Rust zwingt den Nutzer stark auf einen vorgegebenen Pfad. Codex bekommt immer irgendetwas zum Kompilieren hin
      Der Nachteil ist, dass es manchmal scheitern sollte, wenn ein idiomatischer Ansatz nicht möglich ist, stattdessen aber eine dumme Implementierung herauskommen kann, die kompiliert und die Anfrage erfüllt
    • Aus LLM-Sicht ist Rusts Schwäche die Kompilierzeit
      LLMs schreiben Code schneller als Menschen, daher fällt die Wartezeit auf die Kompilierung relativ stärker ins Gewicht. Bei Projekten einer gewissen Größe, etwa ab 100.000 Zeilen, beginnt Rusts ungefähr 10-fach langsamere Kompilierung zum Flaschenhals zu werden
      Wenn man Kerninfrastruktur schreibt, lohnt sich dieser Preis, aber bei internen Services, die nicht öffentlich im Internet stehen, ist Entwicklungsgeschwindigkeit womöglich wichtiger
      Ich denke, langsame Kompilierung beeinflusst auch die Entwicklungsgeschwindigkeit von Menschen, aber seltsamerweise versuchen nur sehr wenige Entwickler, das zu quantifizieren
  • Wenn Umständlichkeit das Hauptproblem ist, könnte das hier, das voraussichtlich in Go 1.28 kommen soll, vieles reduzieren
    https://github.com/golang/go/issues/12854#issue-110104883

  • Die Formulierung „ein Service, von dem die Organisation abhängt, der hohe Verfügbarkeit braucht und geschäftskritisch ist“ ist lustig
    Besonders dann, wenn dieser Rust-Service auf Kubernetes läuft

  • Ich nutze bereits Rust und habe keine Go-Erfahrung, daher ist der Artikel für mich vielleicht nicht ideal zugeschnitten
    Eine Sache stößt mir aber auf. Zu sagen, dass Datenrennen in Rust „zur Compile-Zeit abgefangen werden“, wirkt zumindest etwas übertrieben
    Diese Formulierung kann den Eindruck erwecken, Rust könne auch Dinge wie Lock-Starvation oder andere Nebenläufigkeitsprobleme behandeln. Das kann es in Wirklichkeit nicht
    Ich weiß, dass Datenrennen ein formaler Begriff mit enger Bedeutung ist, aber ich finde trotzdem, dass man das klarer formulieren könnte