Rust oder Go für Backend-Dienste: Nur die benötigten Garantien bezahlen
Warum die Sicherheit und Geschwindigkeit von Rust selten die wirklichen Engpässe bei API-Teams beseitigen, wie Go für Wartbarkeit optimiert wird und wann Rust die richtige Standardwahl für den Backend-Bereich ist.
Rust gehört zu den beeindruckendsten Programmiersprachen des letzten Jahrzehnts, und genau deshalb verdient sie eine sorgfältige Betrachtung, bevor sie zur Standardwahl für Backend-Teams wird. Die meisten Backend-Dienste werden nicht durch die Eigenschaften eingeschränkt, die Rust besonders hervorhebt; vielmehr werden sie durch die Geschwindigkeit begrenzt, mit der eine sich ständig verändernde Gruppe von Personen sie verstehen und sicher modifizieren kann. Dieser Artikel vergleicht Rust und Go aus dieser Perspektive, zeigt auf, wo die tatsächlichen Nachteile jeder Sprache auftreten, und gibt Ihnen eine klare Orientierung dafür, wann die Vorteile von Rust es wert sind, dafür zu zahlen.
Ein Szenario, das man beachten sollte
Stellen Sie sich ein Team vor, das in Rust eine gewöhnliche Backend-Funktion implementiert. Der resultierende Code ist sicherer, disziplinierter und verfügt über stärkere Garantien zur Kompilierzeit als das entsprechende Go-Äquivalent. Zudem dauert die Entwicklung deutlich länger, es entstehen lange Diskussionsstränge zu Typen und Lebenszeiten, und eine routinemäßige Änderung wird zu einer Designdiskussion. Dasselbe Feature in Go wäre schnell verfügbar gewesen und für jeden im Team leicht lesbar.
Aus keinem dieser Ergebnisse folgt, dass eine Sprache schlechter ist. Es bedeutet vielmehr, dass der Aufwand einer Tool gegen das Problem abgewogen werden muss, für das es eingesetzt wird.
Genialität ist nicht dasselbe wie Eignung
Rust hat die Art und Weise, wie die Branche über Sicherheit, Leistung und Korrektheit denkt, verändert – ohne auf einen Garbage Collector angewiesen zu sein. Dieser Erfolg ist real und rechtfertigt den Respekt, den Rust genießt.
Aber die meisten Backend-Teams arbeiten nicht an Speichermotoren, Kernen, Browsern, Spielenginen, eingebettetem Firmware oder sicherheitskritischer Infrastruktur, wo jede Speicherallokation und jeder Speicherbereich von entscheidender Bedeutung sind. Die meisten entwickeln stattdessen APIs. Ihre tägliche Arbeit besteht darin, JSON-Daten zwischen Postgres, Redis, Kafka, S3, Zahlungsgateways, Benachrichtigungsdiensten, internen Systemen sowie Drittanbieter-APIs hin- und herzusenden – wobei diese APIs zu den ungünstigsten Zeiten auf kreative Weise versagen können.
Diese Arbeit ist nicht besonders attraktiv: Geschäftsregeln, Wiederholungsversuche, Protokollierung, Dashboards, Bereitstellungen, Migrationsprozesse sowie der Versuch der Entwickler, die Produktivumgebung stabil zu halten. Für solche Umgebungen kann Rust eine hervorragende Lösung für ein Problem sein, das das Team ursprünglich gar nicht gestellt hat.
Die richtige Frage stellen
Die übliche Sichtweise ist starr und einseitig. Eine Fraktion behauptet, Go sei zu vereinfacht; die andere meint, Rust sei zu komplex. Beide Ansichten sind zutreffend – doch beide verfehlen den Kern der Sache.
„Ist Rust besser als Go?“ ist zu vage, um darauf antworten zu können. Eine Motorsäge ist beim Fällen von Bäumen leistungsfähiger als ein Küchenmesser, aber niemand nimmt eine Motorsäge mit an den Esstisch. Der sinnvolle Vergleich liegt darin, wofür jede Sprache optimiert ist:
- Rust bietet Leistung und Präzision und versucht, ganze Kategorien von Fehlern bereits vor dem Ausführen des Programms auszuschließen.
- Go bietet Zurückhaltung und schnelles Verständnis und ist unter der Annahme entworfen, dass gewöhnliche Ingenieure unter normalen Zeitdruckbedingungen den Code warten werden.
Diese zweite Annahme hat mehr Gewicht, als es auf den ersten Blick scheint.
Was ändert sich, wenn Wartung in die Diskussion einfließt?
Viele Go-Entwickler haben nichts gegen Rust. Einige bewundern es, einige programmieren damit, wieder andere möchten es lernen, und viele sind sich einig, dass es die bessere Wahl für anspruchsvolle Arbeit auf niedriger Ebene ist. Der Ton ändert sich, wenn das Thema vom Sprachdesign zur langfristigen Wartung von Backend-Systemen wechselt.
Zu diesem Zeitpunkt streitet niemand mehr darum, dass Rust leistungsstark ist. Die Frage lautet vielmehr, ob ein typisches Backend-Team die Kosten von Rust in Kauf nehmen sollte, um Probleme zu lösen, mit denen seine Dienste meist gar nicht konfrontiert sind.
Dies ist der Punkt, an dem Go zu einem starken Konkurrenten wird. Nicht weil es fortschrittlicher ist – was nicht der Fall ist. Nicht weil sein Typensystem umfangreicher ist – was ebenfalls nicht zutrifft. Und sicherlich auch nicht, weil es Entwicklern das Gefühl gibt, klug zu sein; im Gegenteil. Go gewinnt, weil es eine unangenehme Wahrheit über Softwareorganisationen widerspiegelt: Die meisten Teams benötigen keinen ausdrucksstärkeren Code. Sie brauchen Code, den mehr Menschen ändern können, ohne Angst davor zu haben.
Das Engpassproblem ist selten die CPU
Backend-Entwickler glauben gerne, ihr System sei nur einer Optimierung von Exzellenz entfernt. Das ist eine schmeichelhafte Vorstellung – meist jedoch falsch.
Die meisten langsamen Backend-Systeme sind nicht langsam aufgrund des Sprachlaufszeiten-Umfelds. Sie sind langsam, weil eine Abfrage ineffizient ist, ein Cache veraltete Daten liefert, das Netzwerk unzuverlässig ist, eine Warteschlange überfüllt ist, eine Abhängigkeit instabil ist oder ein Geschäftsablauf fünf Operationen miteinander verknüpft, die eigentlich unabhängig sein sollten. Eine schnellere Sprache behebt keines dieser Probleme.
Der schwierige Teil der Backend-Entwicklung besteht in der Regel nicht darin, den Rechner dazu zu bringen, Anweisungen auszuführen. Es geht vielmehr darum, einer Gruppe von Personen zu helfen, das System tief genug zu verstehen, damit sie es ohne Schäden modifizieren können. Das untenstehende Diagramm verdeutlicht dies, indem es aufzeigt, wo die eigentlichen Hindernisse typischerweise liegen:
A Normal Backend Team's Real Bottleneck
CPU
|
| usually not here
v
-----------------
| application |
-----------------
| | |
v v v
Postgres Redis Kafka
| | |
v v v
unclear ownership
changing product rules
missing observability
slow code reviews
fear of refactoring
tired on-call engineers
The machine was rarely the hard part.
The humans were.
Das erklärt, warum Rust trotz technischer Überlegenheit für viele Backend-Teams weiterhin keine gute Ausgangslösung darstellt. Rust setzt auf Korrektheit dort, wo Code auf Code trifft. Go optimiert hingegen häufig dafür, am Rande des Teams zu überleben. Dieser Kontrast fasst die gesamte Debatte in einem Satz zusammen.
Go’s Einfachheit ist eine bewusste Einschränkung
Ein typischer Go-HTTP-Handler weist keinerlei Eleganz auf. Er decodiert den Anfragekörper, gibt bei Fehlschlag der Dekodierung einen 400-Status zurück, ruft einen Dienst auf, gibt bei dessen Fehlschlag einen 500-Status zurück und schreibt ansonsten das JSON-Ergebnis aus:
func CreateOrder(w http.ResponseWriter, r *http.Request) {
var req CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, "invalid request", http.StatusBadRequest)
return
}
order, err := service.CreateOrder(r.Context(), req)
if err != nil {
writeError(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, order)
}
Niemand würde das als die Zukunft der Programmierung bezeichnen, doch fast jeder Backend-Entwickler kann es sofort lesen. Ein Junior-Ingenieur kann den Code Zeile für Zeile nachvollziehen. Ein erfahrener Reviewer kann ihn in einer Minute freigeben. Ein Neuzugang kann den Ablauf ohne Anleitung verstehen. Jemand, der ein Problem debuggt, kann genau erkennen, wo eine Anfrage eingeht, wo sie fehlschlagen kann und wo sie wieder herauskommt.
Diese Lesbarkeit ist kein geringfügiger Vorteil; in vielen Organisationen ist sie das Wichtigste überhaupt. Der Codeausschnitt zeigt außerdem, wie offensichtlich Go’s Probleme sind: Das Direktzurückgeben von err.Error() an den Client kann interne Details wie Datenbanknachrichten preisgeben, während man in einem echten Service den Fehler protokollieren und stattdessen eine allgemeine Nachricht senden würde. Der Fehler ist leicht zu erkennen, weil nichts versteckt wird.
Der Wert von begrenzten, klugen Entscheidungen
Erfahrungen mit lang lebenden Systemen neigen dazu, die Bewunderung für Cleverness zu verringern. Was Respekt einbringt, ist langweiliger Code mit offensichtlichen Fehlerstellen – Code, für den der ursprüngliche Autor keine Erklärungen abgeben muss.
Go ist auf eine nützliche Weise unattraktiv: Es beschränkt die Anzahl der komplexen Entscheidungen, die ein Team treffen kann. Das klingt nach Kritik – bis man einen Codebase mit vielen solchen komplexen Entscheidungen betreuen muss. Jede Abstraktion schien bei ihrer Einführung sinnvoll zu sein. Jeder generische Hilfsfunktion hatte einen guten Grund zur Existenz. Jede Framework-Wahl hatte überzeugende Argumente. Doch dann wechselten die Menschen, die Anforderungen änderten sich, und der Codebase verwandelte sich in eine Sammlung vergangener Zuversicht, die niemand mehr vollständig versteht.
Go’s bewusste Schlichtheit setzt sich gegen diesen Trend durch. Das gelingt jedoch nicht immer. Schlechter Go-Code ist weit verbreitet: doppelte Fehlerbehandlung, schwache Domänenmodelle, globale Zustände, Datenkonflikte, Interfaces, die dort verwendet werden, wo sie nicht benötigt werden, sowie context.Context, der überall in Threads eingesetzt wird, ohne dass wirklich verstanden wird, wie Abbrüche funktionieren. Der Unterschied besteht darin, dass Go’s Unordnung in der Regel offensichtlich ist, während Rust’s Unordnung weitaus raffinierter sein kann. Das ist kein Mangel bei Rust – es geschieht einfach, wenn eine Sprache fähigen Ingenieuren mehr Raum gibt, ihre Fähigkeiten zu zeigen.
Wenn einfache Aufgaben eine komplexe Form annehmen
Dies ist der Punkt, an dem Rust für Backend-Teams problematisch werden kann. Die Aufgabe selbst mag trivial sein, doch die damit verbundenen Typen könnten es nicht sein. Die folgende Funktion verarbeitet eine Gruppe von Elementen sequenziell, wartet auf einen asynchronen Handler für jedes Element und stoppt bei dem ersten Fehler:
use std::future::Future;
trait Processable {
type Output: Send + 'static;
}
async fn process_batch<F, Fut, T, E>(
items: Vec<T>,
handler: F,
) -> Result<Vec<T::Output>, E>
where
F: Fn(T) -> Fut + Send + Sync + Clone + 'static,
Fut: Future<Output = Result<T::Output, E>> + Send + 'static,
T: Processable + Send + 'static,
E: Send + 'static,
{
let mut output = Vec::new();
for item in items {
output.push(handler(item).await?);
}
Ok(output)
}
Die Logik besteht aus einem einfachen Schleifenmechanismus. Die Signatur muss jedoch klar angeben, dass der Handler aufrufbar, klonierbar ist und sicher über Threads gesendet sowie geteilt werden kann; dass die von ihm zurückgegebene Future vom Typ Send und 'static ist; sowie dass die Typen für Elemente und Fehler dieselben Grenzen erfüllen. Das ist kein schlechter oder künstlich konstruierter Rust-Code. Sobald async-Code, generische Handler, gemeinsame Grenzen, gestartete Aufgaben, Fehlertypen und Lebenszeiten miteinander kombiniert werden, verlangt Rust von Ihnen, explizit anzugeben, was andere Backend-Sprachen implizit lassen.
Diese Explizitheit hat tatsächlich Wert, und manchmal ist genau das das, was ein System benötigt. Sie ist jedoch nicht kostenlos. Die Kosten zeigen sich bei der Einführung, bei Code-Reviews sowie immer dann, wenn eine Funktion, die aus produktlicher Sicht einfach erscheint, aus Sicht des Typsystems aufwändig zu handhaben ist. Sie treten auch dann auf, wenn ein Entwickler mehr Mühe darauf verwendet, den Compiler davon zu überzeugen, eine Lösung zu akzeptieren, als darüber nachzudenken, ob die Lösung überhaupt notwendig ist.
Besser – aber besser in welcher Hinsicht?
Rust-Befürworter behaupten, dass dieser Widerstand zu besseren Systemen führt, und manchmal haben sie recht. Ein Backend-Team sollte eine präzisere Frage stellen: Besser in welcher Dimension?
- Memorsicherheit: möglicherweise, obwohl auch Go memorsicher ist – abgesehen von Datenkollisionen und der expliziten Verwendung von
unsafe. - Leistung: oft.
- Verhinderung bestimmter Konkurrenzfehler: ja, in vielen Fällen.
Die letzte Dimension ist die, anhand derer meist Backend-Teams bewertet werden, und genau hier ist Rusts Vorteil am wenigsten selbstverständlich.
Wartung – nicht die Erstellung – ist die eigentliche Kostenfaktor
Ein hervorragender Rust-Entwickler kann ausgezeichnete Rust-Systeme erstellen. Daran besteht kein Zweifel. Das Problem ist, dass Organisationen ihr Team nicht in dem Moment einfrieren können, in dem es diesen Entwickler beherbergt. Menschen kommen und gehen, Fristen ändern sich, Produkte entwickeln sich weiter, und wer die ursprüngliche Architektur entworfen hat, wird befördert, geht in den Burnout oder wechselt zu einer anderen Gruppe. Der Code bleibt jedoch bestehen.
Zu diesem Zeitpunkt geht es bei der Wahl der Programmiersprache weniger um Eleganz und mehr um soziale Langlebigkeit. Einige Fragen fassen das zusammen:
- Kann der nächste Entwickler diesen Code verstehen?
- Kann das Team ihn schrittweise überarbeiten?
- Kann ein müder On-Call-Ingenieur es sicher ändern, ohne das gesamte Typenmodell im Kopf behalten zu müssen?
- Wie schnell wird ein Neuzugang produktiv?
- Hält das System im Durchschnitt an Tagen durch, nicht nur in idealen Fällen?
Go neigt dazu, diese Fragen positiver zu beantworten. Nicht weil Go-Entwickler fähiger wären oder weil Go-Code von Natur aus sauber ist, sondern weil die Sprache ihre Oberfläche klein hält und weniger Möglichkeiten bietet, in denen Komplexität sich verstecken kann. Genau deshalb finden einige Ingenieure sie frustrierend. Go schmeichelt seinen Nutzern nicht. Rust kann das Gefühl vermitteln, etwas Bedeutendes zu bauen, während Go das Gefühl erwecken kann, man würde nur Rohrleitungen instand setzen.
Backend-Engineering ist größtenteils Rohrleitungsarbeit. Das Wasser muss fließen, die Rohre müssen leicht auffindbar sein, und die nächste Person sollte in der Lage sein, einen Ventil zu wechseln, ohne erst die gesamte Geschichte des Gebäudes kennen zu müssen.
Wo Rust eindeutig das richtige Werkzeug ist
Das macht Rust jedoch nicht überall überflüssig. Wenn Leistung, Speichersicherheit und niedriges Kontrollniveau zentral für das sind, was man entwickelt, verdient Rust ernsthafte Berücksichtigung. Wenn Abstürze inakzeptabel sind, wenn Fehler bezüglich der Speichersicherheit ein Sicherheitsrisiko darstellen oder wenn Latenz das Endprodukt und nicht nur eine Messgröße ist, könnte Rust die sinnvollste Wahl sein.
Proxys, Datenbanken, Sprachlaufzeiten, Sicherheitstools, eingebettete Systeme, hochleistungsstarke Netzwerke, Entwicklerwerkzeuge sowie einige Infrastrukturdienste gehören alle in diese Kategorie. In solchen Fällen ist die Wahl von Rust eine reif überlegte ingenieurtechnische Entscheidung.
Eine Standard-Backend-API wird jedoch nicht reifer, nur weil sie in Rust geschrieben ist. Manchmal wird sie sogar teurer. Teams wählen Rust aus guten Gründen, aber auch deshalb, weil Go zu einfach erscheint, Java zu korporativ, Python zu unstrukturiert – und Rust wie die Wahl wirkt, die ernsthafte Ingenieure treffen sollten. Das ist kein ingenieurtechnisches Urteil, sondern ästhetische Unsicherheit, die als Entscheidung im Bereich der Systemprogrammierung getarnt wird.
Eine schnelle Entscheidungshilfe
Rust ist die geeignete Wahl, wenn die meisten der folgenden Bedingungen zutreffen:
- der Dienst gehört zur Infrastruktur, wobei Leistung oder Speicherkontrolle Teil des Produkts sind,
- das Team bereits mehrere erfahrene Rust-Entwickler hat und weitere einstellen kann,
- eine Absturz- oder Speichersicherheitsfehler ernsthafte Sicherheits- oder Geschäftsfolgen haben.
Go oder eine andere Sprache, die Lesbarkeit priorisiert, ist die sicherere Wahl, wenn die meisten der folgenden Bedingungen zutreffen:
- Der Dienst überträgt hauptsächlich Daten zwischen Datenbanken, Warteschlangen und APIs,
- das Team verfügt über gemischte Erfahrungen und weist eine hohe Fluktuation auf,
- die Hauptrisiken sind unklare Verantwortlichkeiten, sich ändernde Anforderungen sowie geringe Überwachbarkeit – und nicht die reine Durchsatzleistung.
Zusammenfassung
Rusts Stärken standen nie in Frage. Entscheidend ist, ob gerade diese Stärken dem Backend-Team fehlen.
Falls das Team aus kompetenten Rust-Entwicklern besteht, die an Infrastrukturen arbeiten, bei denen Rusts Garantien direkt auf die Risiken übertragen werden, sollte man ohne Zögern Rust wählen; das geeignetere Werkzeug zu nutzen, ist immer dann richtig, wenn die Aufgabe es erfordert. Wenn das Team hingegen konventionelle Dienste entwickelt, die Daten zwischen Speichern, Warteschlangen, APIs, Dashboards und internen Tools verschieben, spricht die Wahl von Rust eher für teuren Geschmack als für Reife.
Go ist nicht deshalb vorzuziehen, weil es leistungsstärker ist. Für viele Backend-Teams ist es jedoch besser, weil es einfacher in der Anwendung ist: leichter zu lesen, für die Rekrutierung geeigneter, einfacher zu überprüfen und einzusetzen sowie leichter im Alltag zu nutzen, sobald die anfängliche Begeisterung nachlässt. Einfach im Alltag zu nutzen bedeutet nicht Misserfolg – es ist genau das, was Produktionsysteme lange nach dem Hype benötigen.
Die Fehler, die Backend-Teams tatsächlich zum Scheitern bringen, entstehen selten durch den Fehlen eines Borrow-Checkers. Sie resultieren aus unklaren Servicegrenzen, nicht-idempotenten Wiederholungsversuchen, Datenbanken, die stillschweigend zur eigentlichen API wurden, Warteschlangen, die alle Design-Kurzwege aufnahmen, Logs, die nur die halbe Geschichte erzählten, sowie Architekturen, die für ein idealisiertes Team entworfen wurden, das nie existiert hat. Rust verhindert viele Arten von Fehlern, kann aber nicht verhindern, dass man an Stelle von Klarheit auf Macht setzt. Deshalb eignet sich Rust für die meisten Backend-Arbeiten nur unzureichend – nicht, weil er schwach ist, sondern weil seine Stärken teuer zu stehen kommen, wenn das zugrundeliegende Problem hauptsächlich menschlicher Natur ist.
Verwandte Literatur
- Edge Isolates und Wasm gegen Node.js: Laufzeit-Vergleiche und Produktionspraktiken — Es wird erläutert, wie V8-Isolaten und WebAssembly an der Edge-Edgepunkte den containerbasierten Node.js übertreffen, anschließend werden die Betriebspraktiken behandelt, die Node.js für den Produktivbetrieb bereit machen.
- REST-APIs für Anfänger: Ressourcen, Methoden, Statuscodes und Zustandslosigkeit — Ein leicht verständlicher Leitfaden darüber, was eine REST-API ist, welche fünf Prinzipien sie funktionieren lassen, wo sie in echten Teams eingesetzt werden und wie man seine erste REST-API erstellt und testet.