Go und Rust: Einsatz moderner Programmiersprachen

Autor / Redakteur: Dipl. Inf. Ing. ETH Silvan Wegmann* / Sebastian Gerstl |

Rust und Go sind zwei moderne Vertreter von Programmiersprachen, die sich an Anforderungen und Bedürfnisse richten, die mit etablierten Sprachen wie Fortran, C oder C++ nicht so leicht umzusetzen sind. Was ist beim Programmierne mit Go oder Rust zu beachten?

Anbieter zum Thema

Go (stabile Version seit 2012) und Rust (stabile Version seit 2015) sind noch relativ jung. Wieso könnte es sich in der Software-Entwicklung lohnen, auf solche modernen Programmiersprachen zu setzen?
Go (stabile Version seit 2012) und Rust (stabile Version seit 2015) sind noch relativ jung. Wieso könnte es sich in der Software-Entwicklung lohnen, auf solche modernen Programmiersprachen zu setzen?
(Bild: Go's new brand / Google – Steve Francia / CC BY 3.0)

Die grundlegende Aufgabe einen Computer auf einer höheren Abstraktionsebene zu programmieren, sollte eigentlich seit Fortran, Cobol, Pascal oder doch mindestens seit C++ gelöst sein. Trotzdem tauchen immer wieder neue Sprachen auf und etablieren sich teilweise sogar auf breiter Basis. Chris Dannen beschreibt in einem Blog-Beitrag woran dies liegen könnte. Technologische (Cloud, Multiprozessoren) und kulturelle (Digitalisierung) Veränderungen, aber auch unsere Erfahrungen haben einen großen Einfluss auf die Anforderungen, die wir an Programmiersprachen und Entwicklungsumgebungen stellen.

In diesem Artikel werden exemplarisch anhand von Go und Rust einige syntaktische und semantische Aspekte moderner Sprachen beleuchtet. Dabei stehen vor allem jene Aspekte im Vordergrund, die einem als C/C++ Entwickler speziell, ungewohnt oder neu vorkommen werden. Alle abgedruckten Beispiele finden sie auch kompilierfertig in einem GitHub Repository.

Bildergalerie
Bildergalerie mit 5 Bildern

Rust ist eine interessante Sprache, da sie besonders den sicherheitsbewussten Systemprogrammierer ansprechen will. Den Embedded-Programmierer sollte besonders ansprechen, dass es mit Redox bereits ein nutzbares und zusätzlich mehrere experimentelle Rust-Betriebssysteme gibt. Zudem kann Rust mit dem LLVM-Backend auch ARM-Code erzeugen.

Auch Go ist eine spannende Sprache, die mit ihrer radikalen Einfachheit den schnellen Einstieg in komplexe Systeme erleichtern will. Sie hat bereits eine etwas größere Verbreitung als Rust erreicht und wird beispielsweise in Docker, der InfluxDB oder dem Backend des offenen IoT Netzwerks „The Thing Network“ eingesetzt.

Was bietet Go für die Software-Entwicklung?

Die Sprache Go ist entstanden, um Entwickler von großen und komplexen Software Systemen besser zu unterstützen. Die Vermeidung von unnötigen Abhängigkeiten soll die Buildzeiten verkürzen und so dem Entwickler schnelleres Feedback von gemachten Änderungen geben. Die starke Reduktion von Sprachmitteln (wenige Schlüsselwörter, keine Templates/Macros, wenige und einfache Paradigmen) soll Entwicklern einen schnellen Einstieg in eine komplexe Codebasis ermöglichen.

Mit dem mitgelieferten gofmt wird zusätzlich für eine einheitliche Formatierung des Quellcodes gesorgt. Dieses nicht konfigurierbare Tool sorgt für einen einheitlichen Codestil über alle Go-Projekte hinweg. Einige herausstehende Merkmale sind:

  • Groß-/Kleinschreibung legt die Sichtbarkeit von Variablen, Typen und Funktionen fest.
  • Abstraktionen werden mit Interfaces umgesetzt.
  • Anstelle von Exceptions werden Fehler als Rückgabewerte von Funktionen kommuniziert. Funktionen können zu diesem Zweck ein Tuple als Rückgabetyp haben.
  • Zur Unterstützung von Multiprocessing und Parallelität bietet Go leichtgewichtige Prozesse (goroutines) und Channels.
  • Zusätzliche Pakete können über Git-Repositories eingebunden werden.
  • Ungenutzte Variablen, Funktionen und Paketabhängigkeiten sind Kompilierfehler.
  • Der Code wird in ein natives Executable kompiliert, welches keine externen Abhängigkeiten hat.

In den folgenden Kapiteln werden anhand von Codebeispielen weitere Merkmale erläutert (siehe Bildergalerie).

Konstanten, Variablen

Konstanten und Variablen in Go.
Konstanten und Variablen in Go.
(Bild: bbv Software Services)

Go ist durchgängig statisch typisiert, kennt aber Typinferenz. Anstelle eines Typs kann für eine Variable auch ein Initialisierungswert angegeben werden. Daraus bestimmt Go dann den statischen Typ dieser Variable. Nicht-explizit initialisierte Variablen werden mit einem Standardwert initialisiert.

Kontrollstrukturen

Go kennt nur das Schlüsselwort ‘for’ für Schlaufen. Mit Hilfe dieses Schlüsselworts werden while-, for-, repeat- und Endlos-Schlaufen programmiert. Für Fallunterscheidungen bietet Go ein ‘if’ sowie ein ‘switch’ an.

Dabei erlaubt Go im Gegensatz zu C/C++ für die einzelnen Fälle innerhalb eines switch beliebige Typen, Bereiche, Zusammenfassungen von mehreren Fällen und Switch aufgrund eines Typs. Die C/C++ typischen ‘break’ am Ende eines ‘case’ sind bei Go implizit und müssen mit ‘fallthrough’ überschrieben werden, wenn ein anderes Verhalten gewünscht ist.

Abstraktion

In vielen anderen Programmiersprachen müssen Interfaces implementiert werden, indem ein Typ explizit von einem solchen ableitet. In C++ beispielsweise muss von einer abstrakten Basisklasse abgeleitet werden und Java muss mit ‘implements’ die Implementierung eines Interfaces erklärt werden. In Go geschieht dies wesentlich einfacher.

Zuerst wird eine Sammlung von Funktionen als Interface erklärt (Zeilen 6-8 im Beispiel). Nun kann mit der Deklaration der Funktion ‘fläche()’ für den Typ ‘kreis’ (Zeilen 22-24 im Beispiel) das Interface ‘form’ implementiert werden. Eine explizite Erklärung, dass diese Funktion zur Implementierung des Interfaces gehört, ist nicht nötig.

Was bietet Rust für die Software-Entwicklung?

Rust ist eine nativ-kompilierte Programmiersprache, welche mit dem Ziel entwickelt wurde, Systemprogrammierung sicherer zu machen. Die Mechanismen, die dieses Ziel unterstützen sollen, wie explizite Lebenszeit-Verwaltung, Borrow-Checker und Move-Semantik, stellen allerdings für den Anfänger große Hürden dar. Die Lernkurve bei Rust ist gerade am Anfang steiler als bei anderen Sprachen. Einige herausstehende Merkmale sind:

  • Variablen sind standardmäßig unveränderlich und müssen explizit als veränderlich markiert werden.
  • Die Lebenszeit von Variablen wird explizit verwaltet, um eine effiziente Speicherverwaltung ohne Garbage-collection zu implementieren.
  • Rust verfolgt zur Kompilierzeit die Menge an gleichzeitigen Borrows (Rust-Variante von C++-Referenzen) und verhindert Schreibzugriffe auf Variablen, für welche es andere Borrows gibt. Dies verhindert Inkonsistenz durch nicht synchronisierte Schreibzugriffe.
  • Abstraktionen werden mit Traits, generischen Funktionen und Strukturen implementiert.
  • Zusätzliche Pakete können über Git-Repositories eingebunden werden.
  • Die zur Kompilierzeit geprüfte Konsistenz funktioniert als Schutz vor gleichzeitigen Speicherzugriffen (Borrow-Checker).
  • Der Code wird in ein natives Executable kompiliert, welches keine externen Abhängigkeiten hat. Es ist allerdings auch möglich, zusätzlich dynamische Bibliotheken zu bauen und zu laden.
Bildergalerie
Bildergalerie mit 5 Bildern

In den folgenden Kapiteln werden anhand von vollständigen Codebeispielen weitere Merkmale erläutert. Die Beispiele enthalten Rust Attribute (Zeilen bestehend aus ‘#![…]’), welche Warnungen reduzieren und die Unterstützung von Umlauten in Variablen aktivieren sollen.

Konstanten, Variablen, Borrows

Konstanten, Variablen und Borrows in Rust.
Konstanten, Variablen und Borrows in Rust.
(Bild: bbv Software Services)

Auch Rust bietet Typinferenz an. Wird aber der Typ einer Variablen explizit angegeben, initialisiert Rust im Gegensatz zu Go diese nicht mit einem Standardwert. Wird ein solcher nicht-initialisierter Wert verwendet, gibt der Compiler einen Fehler aus (Zeile 17 im Beispiel). Deklarierte Variablen sind standardmäßig unveränderlich und müssen explizit mit ‘mut’ als veränderlich markiert werden (Zeilen 12-15 im Beispiel).

Rust kennt für komplexe Datentypen, also z.B. Structs die Move-Semantik. Eine Variablenzuweisung bedeutet, dass der entsprechende Wert verschoben wird und die ursprüngliche Variable den Wert nicht mehr enthält. Der Compiler überprüft dies und zeigt einen Fehler an, wenn auf eine solche verschobene Variable zugegriffen wird (Zeilen 19-21 im Beispiel).

Ähnlich zu C++ kennt Rust das Prinzip von Referenzen. In Rust werden diese aber Borrows genannt. Wie im Beispiel oben zu sehen ist, werden solche Borrows mit dem ‘&’-Operator erzeugt. Solange nun mindestens ein anderes Borrow auf eine Variable existiert, erlaubt Rust nicht, die ursprüngliche Variable zu verändern (Zeilen 23-33 im Beispiel). Dies verhindert Inkonsistenzen bei gleichzeitigen Schreib-/Lesezugriffen.

Traits, Trait bounds, Trait objects

In Rust werden Interfaces durch einen ‘trait’ deklariert (Zeilen 6-8 im Beispiel). Diese Traits können nun für existierende Typen implementiert werden. Im Normalfall geschieht dies für Structs (Zeilen 15-19 und 25-29 im Beispiel). Rust erlaubt für spezielle Fälle aber auch die Implementierung eines Traits für eingebaute Typen wie i32, float32, String etc.

Es ist zu erkennen, dass in Rust Structs reine Datencontainer darstellen und die Methoden quasi „von außen“ dazu gefügt werden. Dies steht im Kontrast zu C++, wo Methoden Teil der Klassendeklaration sein müssen. Nun gibt es zwei Möglichkeiten, Objekte zu nutzen, welche diese Interfaces implementieren.

Zum einen können Interface-Methoden über sogenannte Trait objects angesprochen werden (Zeilen 31-34 im Beispiel). Die Funktion ‘fläche_ausgeben()’ existiert im kompilierten Code nur einmal und die Funktion ‘fläche()’ wird basierend auf dem Laufzeittyp von f aufgerufen. Dies ist in etwa vergleichbar mit virtuellen Methoden in C++.

Zum anderen können Interface-Methoden über Trait bounds angesprochen werden (Zeilen 36-39 im Beispiel). Die Funktion ‘fläche_ausgeben2()’ existiert im kompilierten Code mehrmals, abhängig davon, für welche Typen sie benötigt wird. Der Aufruf der Funktion ‘fläche()’ geschieht dann statisch aufgrund des Compilezeittyps von f. Dies kann als eine Kombination verstanden werden von C++ Template-Funktionen und den bisher noch nicht in den Standard eingeflossenen Concepts.

Sprachen lernen

In diesem Artikel kann nur ein kleiner Ausschnitt gezeigt werden, was mit Rust und Go alles möglich ist. Viele Aspekte erschließen sich dem Neuling erst, wenn er eine konkrete Aufgabe löst. Dabei muss man häufig Herangehensweisen, bekannt aus der eigenen Erfahrung mit C und C++, über den Haufen werfen und lernen welche Muster und Methoden die neue Sprache für ein solches Problem anbietet.

Für beide Sprachen ist umfangreiches Lernmaterial verfügbar. Für den Einstieg gibt es gute Dokumente bzw. E-Books, welche die Grundlagen einfach und verständlich vermitteln, wie z.B. das „The Little Go Book“ für Go und das von O’Reilly kostenlos angebotene Buch „Why Rust?“ für Rust. Für die praktische Übung von Programmiersprachen eignet sich die Plattform exercism sehr gut.

Zurzeit sind neben Go und Rust über 30 weitere Sprachen verfügbar. Hier können eigene Lösungen mit den Lösungen anderer Teilnehmer verglichen werden und die Teilnehmer können sich gegenseitig mit kleinen Codereviews weiterhelfen. Der Nachteil von exercism liegt darin, dass die meisten Übungen eher allgemein gehalten sind und die sprachspezifischen Möglichkeiten und Eigenschaften weniger hervorgehoben werden.

Ein beliebtes Mittel um mit den Sprachen zu experimentieren ohne gleich selbst eine Entwicklungsumgebung zu installieren, stellen sogenannte Playgrounds dar. Dies sind Webseiten, auf denen kleine Programme in einem Eingabefeld eingetippt und anschließend auf dem Server kompiliert werden können. Solche Playgrounds sind sowohl für Go als auch für Rust verfügbar.

* Silvan Wegmann ist seit 2011 bei der bbv Software Services AG als Senior Embedded Software Engineer tätig. Seine langjährige Erfahrung in den Bereichen Python, C++, Qt/QML und Softwarearchitektur konnte er schon in zahlreichen Projekten für den Erfolg der Kunden einsetzen.

(ID:44847013)