Die noch relativ jungen Programmiersprachen Rust und Go stellen sich mit ähnlichen Konzepten und einer schlanken Syntax den etablierten Kollegen entgegen. Wir zeigen, wo die beiden in der Praxis punkten.
Der Entwickler Graydon Hoare begann 2006 mit der Arbeit an einer neuen Programmiersprache. Unter anderem sollte die in ihr geschriebene Software möglichst flott laufen und Programmierern das fehleranfällige Verwalten des Speichers abnehmen. Die Mozilla Foundation erkannte das Potenzial der Rust getauften Sprache und übernahm drei Jahre später die Leitung des Projekts. Eine erste stabile Version erschien allerdings erst nach mehreren Überarbeitungen im Mai 2015 [1].
Stahlbau
Wie bei C und C++ übersetzt ein Compiler den in Rust geschriebenen Quellcode in ein natives Programm. Die Entwickler versuchen derzeit nach jeweils sechs Wochen eine neue Compiler-Version zu veröffentlichen, die mitunter neue Features mitbringt. Sie sollten daher einmal die aktuelle Version per Hand installieren, was folgender Befehl erledigt:
curl https://sh.rustup.rs -sSf | sh
Anschließend melden Sie sich einmal ab und wieder an. Mit rustup update halten Sie zukünftig das Werkzeug immer auf den aktuellen Stand. Um den Compiler auszuprobieren, speichern Sie Listing 1 in der Textdatei beispiel.rs und setzen dann den Compiler mit rustc beispiel.rs darauf an.
Listing 1
use std::io;
fn lies_zahl() -> u32 {
let mut eingabe = String::new();
io::stdin().read_line(&mut eingabe)
.expect("Konnte Eingabe nicht lesen.");
let eingabe: u32 = eingabe.trim().parse()
.expect("Bitte eine Zahl eingeben!");
eingabe
}
struct Raum {
breite: u32,
laenge: u32,
}
impl Raum {
fn flaeche(&self) -> u32 {
self.breite * self.laenge
}
}
fn main() {
// Anzahl der Räume abfragen:
println!("Anzahl Räume eingeben:");
let anzahl = lies_zahl();
// Für jeden Raum die Breite und Höhe abfragen:
let mut raeume: Vec<Raum> = Vec::new();
for a in 1..anzahl+1 {
println!("Breite von Raum {} eingeben: ", a);
let _breite = lies_zahl();
println!("Länge von Raum {} eingeben: ", a);
let _laenge = lies_zahl();
let raum = Raum { breite: _breite, laenge: _laenge };
raeume.push(raum);
}
// Die Gesamtquadratmeter berechnen:
let mut gesamt = 0;
for r in raeume {
gesamt = gesamt + r.flaeche();
}
println!("Gesamtquadratmeter: {}", gesamt);
}
Das Rust-Programm aus Listing 1 berechnet die Quadratmeterzahl einer Wohnung. Dazu erfragt es zunächst die Anzahl der Räume und dann für jeden Raum die Breite und Länge. Um diese Aufgaben zu lösen, definiert Listing 1 zunächst in Zeile 3 mit dem Schlüsselwort fn eine neue Funktion namens lies_zahl(). Sie fordert die Eingabe einer Zahl und gibt diese zurück.
Welche Daten eine Funktion zurückgibt, verrät die Angabe hinter dem stilisierten Pfeil (->). Das Kürzel u32 steht dabei für eine 32-Bit lange Ganzzahl ohne Vorzeichen. Rust bietet darüber hinaus Typen, die sich an der Architektur orientieren. So ist eine Zahl vom Typ usize nur auf 64-Bit-Systemen eine 64-Bit-Zahl.
Die Funktion lies_zahl() benötigt keine weiteren Informationen für ihre Arbeit. Sollte das bei einer Funktion dennoch der Fall sein, notieren Sie die Parameter in den runden Klammern, wobei der Typ jeweils hinter dem Variablennamen steht:
fn add(x: u32, y: u32) -> u32 { x+y; }
Die Funktion lies_zahl() legt in Zeile 4 die Variable eingabe an, wobei sie gleich zwei Rust-Spezialitäten nutzt. Variablen erstellen Sie in Rust mit dem Schlüsselwort let, wobei normalerweise der Typ der Variablen hinter dem Variablennamen steht. Im folgenden Beispiel würde in der neuen Variablen zwei eine 2 speichern:
let zwei: u32 = 2;
Die so erstellte Variable dürfen Sie allerdings nicht mehr ändern (sie ist unveränderlich (engl.: immutable). Die Zuweisung zwei = 3; würde einen Fehler beim Kompilieren verursachen. Das Schlüsselwort mut ändert dies. Es macht die Variable veränderlich (engl.: mutable), womit das Zuweisen von neuem Inhalt funktioniert. Den Typ der Variablen dürfen Sie weglassen, wenn der Compiler in der Lage ist, diesen eindeutig abzuleiten.
Von dieser Automatik macht Listing 1 Gebrauch: Es ruft die Funktion String::new() auf, die eine leere Zeichenkette liefert. Die zwei Doppelpunkte weisen darauf hin, dass new() eine sogenannte Associated Function des Datentyps String ist – in anderen Programmiersprachen hießen diese etwa statische Methoden. Da somit eindeutig ist, dass eingabe einen String enthält, entfällt die Angabe des Typs bei der Definition der Variablen.
In Zeile 5 holt Listing 1 die Eingabe ab. Das übernimmt praktischerweise eine mitgelieferte Funktion aus der Standardbibliothek. Die in ihr enthaltenen Funktionen sind in sogenannte Module aufgeteilt. Den Namen des Moduls stellen Sie normalerweise dem Funktionsaufruf voran:
std::io::stdin().read_line(&mut eingabe)
Durch use std:io; ganz am Anfang von Listing 1 sparen Sie sich die Angabe des entsprechenden Moduls jedoch in diesem Fall und rufen io::stdin() direkt auf.
Ketten bilden
Die mitgelieferten Datentypen besitzen ein paar nützliche Funktionen. So bietet etwa jede Integer-Zahl die Funktion pow() an, mit der Sie die Potenz bilden:
let res = zwei.pow(5); // 2 hoch 5
Dieses Prinzip kommt bei der Eingabe zum Einsatz: Die Funktion stdin() liefert eine Instanz von Stdin. Die wiederum bietet unter anderem die Funktion read_line(), die auf eine Eingabe von Daten wartet und diese dann in der ihr übergebenen Variablen ablegt.
Über die Punkt-Notation schalten Sie mehrere Funktionsaufrufe hintereinander: In Listing 1 liefert stdin() zunächst eine Instanz von Stdin zurück, in der Rust umgehend die Funktion read_line() aufruft.
Normalerweise überreicht Rust immer eine Kopie des Inhalts einer Variablen an eine Funktion. Im Beispiel ist es jedoch nötig, dass read_line() in der Lage ist, den Inhalt von eingabe zu verändern. Dies gestattet das vorangestellte &mut. Damit übergeben Sie der Funktion eine sogenannte Referenz auf die Variable, die ihr dann den direkten Zugriff auf den Inhalt erlaubt. Bei einer Referenz handelt es sich übrigens nicht um Zeiger (Pointer), wie C oder C++ sie verwenden.
Die Funktion read_line() legt den eingetippten Text in der Variablen eingabe ab und liefert dann eine Instanz vom Typ io::Result zurück. Dieser kapselt einen eventuell aufgetretenen Fehler. Sollte ein solcher vorliegen, gibt die Funktion .expect() den in Anführungszeichen angegebenen Text aus und bricht das Verarbeiten ab. Wie Zeile 6 zeigt, dürfen Sie verkettete Funktionsaufrufe in mehrere Zeilen umbrechen.
Licht und Schatten
Der Code in Zeile 7 wandelt die Texteingabe in eine 32-Bit-Ganzzahl um, die in einer neuen Variablen eingabe landet. Sie überdeckt einfach die alte, gleichnamige Variable. Auf diese Weise sieht die Software somit nur noch die neue Version von eingabe, die eine Zahl speichert. Mit diesem Shadowing genannten Konzept ändern Sie in Rust recht elegant den Typ einer Variablen.
Sofern Sie die Arbeit einer Funktion nicht explizit mit dem Schlüsselwort return abbrechen, liefert diese immer den letzten Ausdruck zurück. In Listing 1 genügt es somit, die Variable mit dem Rückgabewert zu notieren.
Jedes Zimmer hat eine Breite und eine Länge. Diese beiden Informationen kapselt das Programm in einem neuen eigenen Datentyp. In Listing 1 geschieht das ab Zeile 12: Dort fasst dieser Datentyp namens Raum, die Variablen breite und laenge zusammen. Damit brauchen Sie nur eine Variable vom Typ Raum anzulegen und greifen dann über den Punktoperator etwa auf die Breite des Bads zu:
let bad = Raum { breite: 5, laenge: 6 };
bad.breite = 2;
Bei Bedarf ergänzen Sie den Typ Raum um einen Satz Funktionen. Dies geschieht in Listing 1 hinter impl Raum in den geschweiften Klammern. Listing 1 definiert dort nur eine Funktion flaeche, welche die Summe der Quadratmeter für den Raum berechnet. Dabei bezeichnet self eine spezielle Variable, die sich später auf den jeweiligen Raum bezieht. Alle hinter impl gesammelten Funktionen gehören zur Datenstruktur und stehen ebenfalls über die Punktnotation bereit. Mit bad.flaeche() erhalten Sie folglich die Quadratmeter für das Badezimmer. Im Zusammenspiel ermöglichen struct und impl so eine rudimentäre objektorientierte Programmierung.
Lange Liste
Zeile 23 definiert die Funktion main(), die das fertige Programm später automatisch beim Start aufruft. Wie in C dient main somit als Einsprungspunkt und ist für jedes Rust-Programm daher Pflicht. Kommentare leiten Sie in Rust mit zwei Schrägstrichen ein, alternativ stehen sie zwischen /* und */.
In der Funktion main() fragt das Programm aus Listing 1 zunächst die Anzahl der Räume ab. Anschließend entsteht eine Variable vom Typ Vec<Raum>. Dieser sogenannte Vektor speichert wie eine Liste beliebig viele Elemente. Die Angabe in den spitzen Klammern verrät dem Compiler, welche Datentypen der Vektor aufnimmt.
Falls Ihnen diese Notation aus anderen Sprachen bekannt vorkommt: Rust bietet ebenfalls generische Typen (Generics), bei denen Sie den zu verwendenden Datentyp erst später festlegen. Nützlich ist das wie im Beispiel vor allem bei Listen und Vektoren, die auf beliebige Datentypen anwendbar sein sollen.
Rundlauf
Nach dem Anlegen eines leeren Vektors fordert Listing 1 vom Benutzer für jeden Raum die Breite und die Länge an. Dabei hilft die For-Schleife, die nach jedem Durchlauf die Variable a erhöht, bis diese den Wert von anzahl+1 erreicht hat. a startet dabei mit dem Wert 1 (Abbildung 1).
Im Rumpf der Schleife gibt println! einen Text aus. Den Inhalt von a platziert println! dabei an der Stelle der geschweiften Klammer. Am Ende der Schleife erstellt Listing 1 einen neuen Raum und schiebt diesen mit der Funktion push() in den Vektor. Zum Schluss durchläuft die zweite for-Schleife den Vektor, berechnet für jeden Raum die Fläche und addiert sie zur Summe der Quadratmeter.
Neben den vorgestellten Konzepten bietet Rust noch unter anderem ein eigenes Build-System mit dem Namen Cargo, While- und Loop-Schleifen sowie die Fallunterscheidung mit If und Match. Darüber hinaus bietet die Sprache Arrays, Hash Maps (Dictionaries), Aufzählungstypen (Enums), Generics, Traits, Closures, Iteratoren sowie Smart Pointer. Sie unterstützt Threads und Nebenläufigkeit, Pattern Matching und enthält ein System zum Behandeln von Fehlern und ein Modulsystem.
Go, Go, Go!
Nur ein Jahr nach Graydon Hoare begannen die Google-Mitarbeiter Robert Griesemer, Rob Pike und Ken Thompson die Arbeit an einer neuen Programmiersprache namens Go [2]. Die drei verfolgten dabei ähnliche Ziele wie ihr Konkurrent, insbesondere wollten sie einige Unzulänglichkeiten von C, C++ und Java eliminieren. Seit 2012 liegt Go in der ersten stabilen Version vor, wobei die Sprache innerhalb einer Hauptversion abwärtskompatibel bleiben soll.
Genau wie bei Rust übersetzt den Go-Quellcode ein Compiler. Diesen halten viele große Distributionen in ihren Repositories vor, unter Ubuntu genügt zur Installation der Befehl sudo apt install golang. Ob der Compiler funktioniert, testen Sie mit Listing 2: Speichern Sie es in der Datei beispiel.go, und verfüttern Sie sie via go build beispiel.go an den Compiler (Abbildung 2).
![Abbildung 2: Im Playground auf der Go-Homepage haben Sie die Möglichkeit, einen kurzen Go-Programmcode einzutippen und direkt im Browser auszuführen <a href="#artRef-i3">[3]</a>.](/wp-content/uploads/2018/12/b02-go_playground-300x186.jpg)
Abbildung 2: Im Playground auf der Go-Homepage haben Sie die Möglichkeit, einen kurzen Go-Programmcode einzutippen und direkt im Browser auszuführen [3].
Listing 2
package main
import (
"fmt"
"os"
"bufio"
"strconv"
"strings"
)
func lies_zahl() int {
reader := bufio.NewReader(os.Stdin)
text, err := reader.ReadString('\n')
if err != nil {
fmt.Printf("Konnte Eingabe nicht lesen!\n", err)
}
text = strings.TrimSuffix(text, "\n")
zahl, err := strconv.Atoi(text)
if err != nil {
fmt.Printf("Konnte Zahl nicht konvertieren!\n", err)
}
return zahl
}
type Raum struct {
breite int
laenge int
}
func (r Raum) flaeche() int {
return r.breite * r.laenge
}
func main() {
// Anzahl der Räume abfragen:
fmt.Println("Anzahl Räume eingeben:")
anzahl := lies_zahl()
// Für jeden Raum die Breite und Höhe abfragen:
var raeume []Raum
for i := 1; i < anzahl+1; i++ {
fmt.Printf("Breite von Raum %d eingeben:\n", i)
_breite := lies_zahl()
fmt.Printf("Länge von Raum %d eingeben:\n", i)
_laenge := lies_zahl()
raum := Raum {_breite, _laenge}
raeume = append(raeume, raum)
}
// Die Gesamtquadratmeter berechnen:
var gesamt int
for _, r := range raeume {
gesamt = gesamt + r.flaeche()
}
fmt.Printf("Gesamtquadratmeter: %d\n", gesamt)
}
Flinker Zollstock
Listing 2 berechnet die Wohnfläche in Go. Bei dieser Sprache leiten Sie ebenfalls Kommentare über zwei Schrägstriche ein, alternativ klemmen Sie sie zwischen /* und */. Ähnlich wie in Java besteht ein Go-Programm aus Paketen. In Listing 2 legt die erste Zeile fest, dass sich der nachfolgende Quellcode in einem Paket namens main befindet, das jedes Go-Programm benötigt.
Mit import holen Sie mehrere Pakete aus der Standardbibliothek hinzu. Sie stellen Funktionen für den Zugriff auf die Standardeingabe (os), dem Einlesen von Daten (bufio), dem Konvertieren von Strings (strconv) und der Manipulation von Strings (strings) bereit.
Da der Nutzer mehrfach eine Zahl eintippen muss, kapselt in Listing 2 die Funktion lies_zahl() alle dazu notwendigen Aktionen. Funktionsdefinitionen leitet das Schlüsselwort func ein. In Go notieren Sie den Typ des Rückgabewertes ebenfalls hinter den runden Klammern. Benötigt die Funktion Parameter, stehen wie in Rust die Datentypen hinter den Variablennamen:
func ggt(a int, b int) int {...}
Über int kennzeichnen Sie eine Ganzzahl mit Vorzeichen, deren Größe von der Architektur abhängt. Auf einem 32-Bit-System ist die Zahl somit 32-Bit groß. Wie bei Rust stehen Datentypen mit fester Größe bereit, int64 repräsentiert etwa eine 64-Bit große Ganzzahl mit Vorzeichen.

Abbildung 3: Der Go-Compiler verweigert die Arbeit, wenn eine Variable zwar definiert ist, Sie diese aber ansonsten nicht nutzen.
Die Funktion lies_zahl() holt zunächst aus dem Paket os mit Stdin eine Repräsentation der Standardeingabe. Die verfüttert sie an die Funktion NewReader() aus dem Paket bufio. Das Ergebnis ist ein sogenannter Reader, mit dem Sie Eingaben von der Standardeingabe lesen. In Go kennzeichnen Sie das Ende einer Anweisung nur dann mit einem Semikolon, wenn dies wirklich erforderlich ist.
Stauraum
Variablen definieren Programmierer mit dem Schlüsselwort var, der Typ steht hinter dem Variablennamen:
var zahl int = 12
Ist der Typ eindeutig, dürfen Sie in Go die Kurzschreibweise zahl := 12 verwenden. Der Compiler ermittelt dann selbstständig den Typ der Variablen. Das funktioniert allerdings nur innerhalb von Funktionen, globale Variablen deklarieren Sie stets mit var.
Mit der Kurzschreibweise parkt lies_zahl() den Reader in der Variablen reader. In Zeile 13 liest dann die Funktion ReadString() so lange Zeichen ein, bis Sie [Eingabe] drücken und somit einen Zeilenumbruch (\n) erzeugen. Die Eingabe landet in der Variablen text, ein eventuell aufgetretener Fehler in der Variablen err. Wie das Beispiel zeigt, dürfen Funktionen mehrere Werte gleichzeitig liefern.
Ein Fehler liegt genau dann vor, wenn die Variable err nicht nil ist. Dann trifft in Zeile 14 die Bedingung hinter if zu. Wie in vielen anderen Sprachen steht != für “nicht gleich”. Im Fehlerfall gibt in Zeile 15 Println() eine Meldung und zusätzlich die Beschreibung eines Fehlers aus.
Bevor das Programm den Text in eine Zahl umwandelt, entfernt die Funktion TrimSuffix() den Zeilenumbruch, der ebenfalls in der Variablen enthalten ist. Anschließend konvertiert Atoi() den Text in eine Ganzzahl. Den dabei in zahl aufgefangenen Wert liefert Zeile 24 zurück.
Alle einsteigen
Wie in Rust dürfen Sie mehrere Informationen in einer Struct zusammenfassen. Listing 2 kapselt so die Breite und die Länge in einer Struct Raum. Das vorangestellte Schlüsselwort type sorgt dafür, dass Raum ab sofort als neuer Typ bekannt ist. Um einen konkreten Raum anzulegen, geben Sie wie in Rust die Daten in geschweiften Klammern an, der Zugriff auf die Elemente erfolgt mit der Punktnotation:
bad := Raum {5, 6}
bad.breite = 2
Wie in Rust ist es möglich, in Go die Structs um Funktionen zu ergänzen, die dann wiederum die gekapselten Daten manipulieren. Go bezeichnet solche Funktionen als Methoden. Listing 2 ergänzt ab Zeile 32 auf diese Weise den Raum um eine Methode, die seine Fläche berechnet. Die Notation weicht hier etwas deutlicher von Rust ab: Hinter dem Schlüsselwort func verrät die Angabe in Klammern, zu welcher Struct die Methode gehört. Go spricht hier vom Empfänger der Methode.
In Listing 2 ist das der Raum. Über den ebenfalls in den Klammern hinterlegten, beliebig wählbaren Namen r ist die Methode in der Lage, auf die Daten zuzugreifen. Hinter den runden Klammern folgt dann die gewohnte Funktionsdefinition. Die Methode gehört damit zu einem Raum, und es ist möglich, diese über den Punktoperator aufzurufen. Die Fläche des oben erstellten Bads berechnet etwa bad.flaeche().
Zerschnittener Rundlauf
Die Funktion main() dient als Einsprungspunkt in das Programm. In ihr fragt das Programm zunächst die Anzahl der Räume ab. Anschließend startet es in Zeile 43 eine For-Schleife. Deren Aufbau folgt ihrem C-Vorbild: Hinter dem Schlüsselwort for steht der Name einer neuen Variablen, die Listing 2 mit dem Wert 1 initialisiert.
Die Schleife läuft so lange, wie die Bedingung hinter dem ersten Semikolon erfüllt ist. Die Anweisung hinter dem zweiten Semikolon führt Go nach jedem Schleifendurchlauf aus. Die Software zählt dort einfach die Variable i um eins hoch. Der Schleifenrumpf fragt die Breite und Länge eines Raums ab und speichert sie in einem neuen Raum.
Wie viele andere Sprachen bietet Go auch Arrays an, die mehrere Elemente eines Typs speichern. Im folgenden Beispiel enthält das Array farben drei Wörter:
farben := [3]string{"Rot", "Grün", "Blau"}
fmt.Println(farben[2])
Der Zugriff auf ein Element erfolgt über die Angabe des Index in eckigen Klammern, wobei die Zählung bei 0 beginnt. Das obige Beispiel würde somit Blau ausgeben. Um die Arrays um weitere Elemente zu ergänzen, wären jedoch einige Verrenkungen notwendig.
Allerdings bietet Go mit den sogenannten Slices eine dynamische Form der Arrays, die Sie über die Funktion append() um zusätzliche Elemente erweitern. Diese Eigenschaft nutzt das Programm, das sich in einem Slice die angelegten Räume merkt. Zeile 42 richtet zunächst ein leeres Slice raeume ein, das Elemente vom Typ Raum speichert. In der For-Schleife fügt dann Zeile 51 den raeumen den zuvor angelegten raum hinzu.
Durchlauf
Die Summe der Quadratmeter des kompletten Hauses berechnet eine zweite Form der For-Schleife in Zeile 56. Sie durchläuft alle Elemente im Slice raeume. Das jeweils aktuelle Element sprechen Sie in der Schleife über den Namen an, der vor dem Operator := steht. Anstelle des Unterstrichs _ dürfen Sie den Namen einer weiteren Variablen angeben:
for i, r := range raeume {...}
Den Inhalt von i zählt die Schleife bei jedem Durchlauf hoch. Sofern Sie diesen Zähler nicht benötigen, ersetzen Sie den Variablennamen wie in Zeile 56 durch den Unterstrich. Im Gegensatz zu Rust kennt Go ausschließlich die For-Schleife.
Die vorletzte Zeile in Listing 2 gibt die Quadratmeterzahl mit Printf() aus, das die Inhalte von Variablen in den Text einbaut. Ein mit % startender Platzhalter zeigt im Text an, wo die Funktion den Inhalt der hinter dem Komma notierten Variablen einbaut. Der hinter % folgende Buchstabe verrät, um welche Daten es sich handelt. %d steht etwa für eine Ganzzahl. Auch Go hält noch einige weitere Sprachelemente bereit, die der Kasten Weiterlaufen auflistet.
Go bietet außerdem noch Fallunterscheidungen mit switch, Zeiger (aber keine Zeigerarithmetik wie in C), Maps (Dictionaries), Interfaces, Closures. Außerdem unterstützt die Sprache Threads und Nebenläufigkeit sowie den Datenaustausch über Kanäle.
Fazit
Rust und Go ähneln sich in vielen Bereichen. Dazu zählt etwa die statische Typisierung, die Typinferenz und der Verzicht auf klassische Objekte und Vererbung. Beide Sprachen holen sich viele Anleihen bei C und funktionalen Sprachen wie Haskell.
Die Mozilla Foundation verwendet Rust derzeit bei der Entwicklung ihres Browsers, während Google Go unter anderem auf seinem Download-Server einsetzt (dl.google.com). Beide Sprachen sind folglich praxistauglich. Laut verschiedener Statistiken scheint allerdings Go derzeit beliebter zu sein als Rust [4].
Aufgrund ihrer Ähnlichkeit hängt die Wahl jedoch vom konkreten Projekt und nicht zuletzt den eigenen Vorlieben ab. Den Einstieg in Rust vermittelt das ausführliche Handbuch [5]. Da die Rust-Entwickler in der Vergangenheit einige Sprachelemente verändert haben, empfiehlt es sich, ältere Anleitungen im Internet zu meiden. An Go interessierte Programmierer führt schnell das interaktive Tutorial in die Sprache ein [6].
Infos
-
Go Playground: https://play.golang.org/
-
Tiobe Index: https://www.tiobe.com/tiobe-index/
-
The Rust Programming Language: https://doc.rust-lang.org/book/
-
Go Tutorial: https://go-tour-de.appspot.com/welcome/1






