cat posts/rust-lernen-01-hallo-compiler.md
Rust lernen (Teil 1): Hallo, Compiler
Rust installieren, das erste Cargo-Projekt anlegen und die ersten Fehlermeldungen lesen. Am Ende zählt logbuch die Zeilen einer Logdatei, mit drei grünen Tests.
Diese Serie richtet sich an Leute, die Python können und Rust lernen wollen. Wenn Du Python lernen durchgearbeitet hast, bringst Du genug mit. Was eine Variable, eine Funktion oder eine Schleife ist, erkläre ich nicht noch einmal.
Erklären muss ich dafür eine Menge anderes. Rust verbietet Dinge, die in Python selbstverständlich sind, und lässt den Code dann gar nicht erst laufen. Der Compiler meckert, und zwar oft. Dafür meckert er ungewöhnlich gut: Seine Fehlermeldungen zeigen auf die Stelle und schlagen meist gleich die Korrektur vor. Darauf baut die Serie auf. Jeder Teil arbeitet sich von Code, der nicht kompiliert, über die Fehlermeldung zum Konzept dahinter vor.
Das Projekt heißt logbuch, ein Log-Analyzer für nginx-Access-Logs. Heute zählt
er Zeilen. Am Ende der Serie wertet er große Logs parallel aus, und Du rufst ihn
aus Python auf.
Rust installieren
Rust installierst Du über rustup. rustup verwaltet die
Toolchains, also Compiler, Standardbibliothek und Werkzeuge, ungefähr so wie
uv python install Python-Versionen verwaltet.
Linux und macOS:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Die Pakete Deiner Distribution sind meist älter als die aktuelle stabile Version. Für diese Serie nimmst Du rustup.
Windows: Lade rustup-init.exe von rustup.rs oder
installiere es mit winget install Rustlang.Rustup. Rust nutzt unter Windows den
Linker aus den MSVC Build Tools von Visual Studio. Fehlen sie, bietet
rustup-init.exe an, sie samt Windows SDK über den Visual-Studio-Installer
nachzuinstallieren.
Öffne danach ein neues Terminal, damit ~/.cargo/bin im PATH landet, und prüfe
die Version:
$ rustc --version
rustc 1.99.1 (58cf8f9c3 2026-10-13)
Die Serie setzt Rust 1.99 oder neuer voraus. Eine neue stabile Version erscheint
alle sechs Wochen. rustup update holt sie.
Das erste Projekt
rustc ist der Compiler, aber direkt rufst Du ihn selten auf. Das übernimmt
Cargo: Build-System, Paketmanager und Test-Runner in einem. Aus Python
kennst Du das als uv, pip, pytest und python -m build zusammen.
$ cargo new logbuch
Creating binary (application) `logbuch` package
...
$ cd logbuch
Cargo legt ein Verzeichnis mit Git-Repository, .gitignore und zwei Dateien an.
Die Cargo.toml entspricht der pyproject.toml:
[package]
name = "logbuch"
version = "0.1.0"
edition = "2024"
[dependencies]
edition legt fest, welche Sprachregeln gelten. Editionen erscheinen etwa alle
drei Jahre und dürfen inkompatible Änderungen an der Sprache einführen.
Bibliotheken verschiedener Editionen lassen sich trotzdem miteinander verwenden.
Die zweite Datei ist src/main.rs:
fn main() {
println!("Hello, world!");
}
fn leitet eine Funktion ein, main ist der Einstiegspunkt. Das Ausrufezeichen
hinter println ist kein Tippfehler: Es kennzeichnet ein Makro (Macro), also
Code, der beim Kompilieren weiteren Code erzeugt. Warum println! ein Macro
(Makro) ist und keine Funktion, siehst Du gleich. Ausführen:
$ cargo run
Compiling logbuch v0.1.0 (/home/du/logbuch)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.69s
Running `target/debug/logbuch`
Hello, world!
Cargo übersetzt den Quelltext in
eine eigenständige Programmdatei unter target/debug/ und startet sie danach.
Python übersetzt auch, aber nur in Bytecode für seine eigene virtuelle Maschine
(Details in Was passiert bei python skript.py?).
Das Rust-Programm läuft dagegen ohne Interpreter. Du kannst target/debug/logbuch
meist auf einen anderen Rechner mit demselben Betriebssystem und derselben
Prozessorarchitektur kopieren und dort starten, auch wenn darauf kein Rust
installiert ist.
Der erste Fehler
Jetzt bekommt der Compiler etwas zu tun. Ersetze den Inhalt von src/main.rs:
fn main() {
let zeilen = 0;
println!("{zeilen} Zeilen");
zeilen = zeilen + 1;
println!("{zeilen} Zeilen");
}
In Python wäre das völlig unauffällig. cargo run antwortet:
error[E0384]: cannot assign twice to immutable variable `zeilen`
--> src/main.rs:4:5
|
2 | let zeilen = 0;
| ------ first assignment to `zeilen`
3 | println!("{zeilen} Zeilen");
4 | zeilen = zeilen + 1;
| ^^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut zeilen = 0;
| +++
For more information about this error, try `rustc --explain E0384`.
So sieht eine Rust-Fehlermeldung aus, und Du wirst viele davon lesen. Oben stehen
der Fehlercode und eine Zeile Klartext. Darunter folgt der Code mit Markierungen:
------ zeigt die Stelle, die zum Fehler beiträgt, ^^^^ die Stelle, an der er
auftritt. Unter help: steht die Korrektur, mit +++ unter dem einzufügenden
Text. rustc --explain E0384 liefert eine ausführliche Erklärung mit Beispielen.
Die findest Du auch im Error-Index.
Inhaltlich sagt die Meldung: Variablen sind in Rust standardmäßig
unveränderlich. let bindet einen Wert an einen Namen, und dabei bleibt es.
Wer ändern will, schreibt let mut und sagt damit allen Lesern: Achtung, dieser
Wert ändert sich noch. In Python gibt es diese Unterscheidung nicht, jeder Name
lässt sich jederzeit neu binden. Mit let mut zeilen = 0; kompiliert das
Programm und gibt 0 Zeilen und 1 Zeilen aus.
Nebenbei siehst Du, warum println! ein Macro ist. "{zeilen} Zeilen" sieht aus
wie ein f-String und funktioniert auch so. Das Macro prüft aber schon beim
Kompilieren, ob es zeilen gibt und ob sich der Wert ausgeben lässt. Ein
Tippfehler im Platzhalter ist ein Compilerfehler, kein Laufzeitfehler. Eine
gewöhnliche Funktion könnte in den String nicht hineinsehen.
Eine Datei lesen
logbuch soll eine Logdatei lesen. Lege im Projektverzeichnis eine access.log
mit diesem Inhalt an. Das Format ist nginx combined, die IP-Adressen stammen aus
den für Dokumentation reservierten Bereichen:
203.0.113.7 - - [02/Nov/2026:08:14:03 +0100] "GET / HTTP/1.1" 200 5123 "-" "Mozilla/5.0 (X11; Linux x86_64)"
198.51.100.23 - - [02/Nov/2026:08:14:05 +0100] "GET /feed.xml HTTP/1.1" 200 18342 "-" "Miniflux/2.2"
203.0.113.7 - - [02/Nov/2026:08:14:09 +0100] "GET /favicon.ico HTTP/1.1" 404 153 "https://geekblog.io/" "Mozilla/5.0 (X11; Linux x86_64)"
192.0.2.44 - - [02/Nov/2026:08:15:31 +0100] "POST /wp-login.php HTTP/1.1" 404 153 "-" "python-requests/2.32.3"
198.51.100.23 - - [02/Nov/2026:08:16:12 +0100] "GET /posts/rust-lernen-01-hallo-compiler HTTP/1.1" 200 24871 "-" "Miniflux/2.2"
In Python wäre das text = open("access.log").read(). Der naheliegende Versuch in
Rust:
use std::fs;
fn main() {
let text: String = fs::read_to_string("access.log");
println!("{text}");
}
use std::fs; entspricht ungefähr from std import fs: Danach ist das Modul
fs aus der Standardbibliothek unter seinem kurzen Namen erreichbar. Anders als
import lädt use nichts, es kürzt nur den Pfad ab. Mit : String sage ich
dem Compiler, welchen Typ ich erwarte. Meist leitet er Typen selbst ab. Hier hilft
die Angabe, weil die Fehlermeldung dann deutlicher wird:
error[E0308]: mismatched types
--> src/main.rs:4:24
|
4 | let text: String = fs::read_to_string("access.log");
| ------ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `String`, found `Result<String, Error>`
| |
| expected due to this
|
= note: expected struct `String`
found enum `Result<String, std::io::Error>`
help: consider using `Result::expect` to unwrap the `Result<String, std::io::Error>` value, panicking if the value is a `Result::Err`
|
4 | let text: String = fs::read_to_string("access.log").expect("REASON");
| +++++++++++++++++
read_to_string gibt keinen String zurück, sondern ein Result. Das ist ein
Behälter, der entweder den Text enthält oder einen Fehler. Die Datei könnte
fehlen, gesperrt sein oder kein gültiges UTF-8 enthalten. Python meldet so etwas
mit einer Exception, die Du abfangen kannst oder auch nicht. Rust hat keine
Exceptions. Der Fehler steckt im Rückgabewert, und Du kommst nicht an den Text,
ohne Dich zum Fehlerfall zu verhalten.
Die einfachste Haltung schlägt der Compiler selbst vor: .expect("…"). Ist alles
gut, liefert expect den Text. Sonst bricht das Programm mit der Meldung ab. Das
heißt in Rust Programmabbruch (Panic). Für ein Werkzeug in Teil 1 reicht das.
Wie man Fehler ordentlich behandelt, ist Thema von Teil 7.
Zeilen zählen, mit Tests
Hier ist der vollständige Stand für heute:
use std::env;
use std::fs;
fn main() {
let pfad = env::args().nth(1).expect("Aufruf: logbuch <DATEI>");
let text = fs::read_to_string(&pfad).expect("Datei nicht lesbar");
let anzahl = zaehle_zeilen(&text);
println!("{pfad}: {anzahl} Zeilen");
}
fn zaehle_zeilen(text: &str) -> usize {
text.lines().count()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zaehlt_zeilen() {
assert_eq!(zaehle_zeilen("eins\nzwei\ndrei\n"), 3);
}
#[test]
fn letzte_zeile_ohne_umbruch_zaehlt_mit() {
assert_eq!(zaehle_zeilen("eins\nzwei"), 2);
}
#[test]
fn leerer_text_hat_keine_zeilen() {
assert_eq!(zaehle_zeilen(""), 0);
}
}
Von oben nach unten:
env::args()liefert die Kommandozeilenargumente, wiesys.argv..nth(1)holt das zweite, denn das erste ist wie in Python der Programmname. Das Argument kann fehlen.nthgibt deshalb wieder einen Behälter zurück, den Du mitexpectöffnest. Der Behälter heißtOption, er kommt in Teil 6 an die Reihe.fn zaehle_zeilen(text: &str) -> usizenimmt einen Text und gibt eine Zahl zurück. Typangaben an Parametern sind in Rust Pflicht, am Rückgabewert ebenfalls, sobald die Funktion etwas zurückgibt. Anders als Type Hints in Python prüft der Compiler sie auch.usizeist eine vorzeichenlose Ganzzahl, mehr zu den Zahltypen in Teil 2.text.lines().count()zerlegt den Text in Zeilen und zählt sie. Steht hinter dem letzten Ausdruck einer Funktion kein Semikolon, ist er ihr Rückgabewert. Einreturnbrauchst Du hier nicht.- Das
&in&str,&pfadund&textheißt: Die Funktion bekommt den Wert geliehen und gibt ihn danach zurück. Lass es testweise bei&pfadweg, dann meldet sich der Compiler. Was dahintersteckt, ist Thema von Teil 3 und 4.
Die Tests stehen in derselben Datei wie der Code, und das ist in Rust so üblich.
#[cfg(test)] sorgt dafür, dass das Modul tests nur beim Testen kompiliert
wird. use super::* holt alles aus dem übergeordneten Modul herein, hier also
aus dem Rest von main.rs. #[test] markiert eine Testfunktion. assert_eq!
vergleicht zwei Werte. Wer pytest kennt, findet sich sofort zurecht:
$ cargo test
...
running 3 tests
test tests::leerer_text_hat_keine_zeilen ... ok
test tests::letzte_zeile_ohne_umbruch_zaehlt_mit ... ok
test tests::zaehlt_zeilen ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Und mit der Beispieldatei:
$ cargo run -- access.log
access.log: 5 Zeilen
Das -- trennt die Argumente für Cargo
von denen für Dein Programm. Fehlt die Datei, endet das Programm mit einem Panic
(Programmabbruch) und Exit-Code 101:
$ cargo run -- gibtsnicht.log
...
thread 'main' (50540) panicked at src/main.rs:6:42:
Datei nicht lesbar: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target/debug/logbuch gibtsnicht.log` (exit status: 101)
Hübsch ist das nicht. Immerhin bricht das Programm genau dort ab, wo Du es festgelegt hast, und nicht drei Funktionen später an einem leeren Wert.
Der Arbeitsrhythmus
Meistens lohnt cargo run gar nicht. cargo check prüft den Code, ohne eine
Programmdatei zu erzeugen, und ist deshalb schneller. In der Praxis arbeitest
Du in einer Schleife: Code ändern, cargo check, Meldung lesen, wieder von vorn.
Für einen Pythonista fühlt sich das anfangs zäh an. Dafür erreichen viele Fehler,
die Python erst zur Laufzeit meldet, in Rust die Laufzeit nie. Was es außer
check noch gibt, von Clippy über rustfmt bis cargo doc, steht im
Begleitartikel zu Cargo, Clippy und rustfmtgeplant.
Übungen
1. Bring diesen Code zum Kompilieren.
fn main() {
let gruss = "Hallo";
println!("{gruss}");
gruss = "Servus";
println!("{gruss}");
}
Entferne danach das erste println! und kompiliere noch einmal. Was meldet der
Compiler jetzt?
Lösung
`gruss` braucht ein `mut`: `let mut gruss = "Hallo";`. Ohne das erste `println!` kompiliert der Code weiterhin, aber der Compiler warnt:warning: value assigned to `gruss` is never read
--> src/main.rs:2:21
|
2 | let mut gruss = "Hallo";
| ^^^^^^^ this value is reassigned later and never used
3 | gruss = "Servus";
| ---------------- `gruss` is overwritten here before the previous value is read
2. Bytes oder Zeichen? Ergänze die Ausgabe von logbuch um die Größe des
Texts mit text.len(). Probiere vorher in einem Test aus, was "Grüße".len()
ergibt. In Python liefert len("Grüße") den Wert 5.
Lösung
`"Grüße".len()` ergibt **7**. Rust-Strings sind UTF-8, und `len()` zählt Bytes, nicht Zeichen. `ü` und `ß` belegen je zwei Bytes. Zeichen zählst Du mit `"Grüße".chars().count()`, das ergibt 5. Für die Dateigröße ist `len()` genau richtig:println!("{pfad}: {anzahl} Zeilen, {} Bytes", text.len());
3. Was zählt lines()? Schreib einen Test für den Text "eins\n\nzwei".
Überlege vorher, welches Ergebnis Du erwartest, und lies die Ausgabe, wenn Du
falsch liegst.
Lösung
Die leere Zeile in der Mitte zählt mit, das Ergebnis ist 3. Wer 2 erwartet, sieht:thread 'tests::leere_zeile' (30452) panicked at src/main.rs:32:9:
assertion `left == right` failed
left: 3
right: 2
Im nächsten Teil
logbuch liest bisher nur Text. In Teil 2 geht es um Typen und Kontrollfluss:
Ganzzahltypen mit fester Breite, was bei einem Überlauf passiert, warum if und
loop in Rust Werte liefern, und wie aus "404" eine Zahl wird. Danach kennt
logbuch den Statuscode jeder Zeile.
Ab Teil 3 wird es grundsätzlich. Dann geht es um Besitz (Ownership). An diesem Konzept scheitern Python-Gewohnheiten am häufigsten.
Tastenkürzel
- j k
- nach unten / oben scrollen
- n p
- nächster / vorheriger Abschnitt
- g G
- Anfang / Ende des Artikels
- t
- Gliederung zeigen
- ?
- diese Hilfe zeigen
- Esc
- schließen
0 Kommentare
Noch keine Kommentare. Sei der/die Erste!
Anmelden um einen Kommentar zu hinterlassen.