Eigenschaften von Go

Go ist eine vergleichsweise neue Programmiersprache, die konsequent für den Praxiseinsatz entwickelt wurde. Sie verzichtet fast vollständig auf die Einführung völlig neuer Features und Paradigmen. Statt dessen werden nützliche Eigenschaften aus anderen Sprachen übernommen und die Vor- und Nachteile sorgfältig abgewogen.

Programmierparadigmen

Go entspricht nicht einem der klassischen Programmierparadigmen, sondern kombiniert Eigenschaften aus verschiedenen Paradigmen.

Es handelt sich um eine imperative Programmiersprache, d.h. die Anweisungen eines Programmes werden in der Reihenfolge des Quelltextes abgearbeitet. Eine Parallelisierung ist sowohl explizit (durch Goroutinen) als auch implizit (durch Reordering im Compiler und/oder der CPU) möglich. Jedoch gibt es bei Zugriffen auf globale Variablen genau definierte Regeln für die Reihenfolge und Synchronizität.

Daneben ist Go modular. Jedes Programm besteht aus einem oder mehreren Modulen (Packages). Module spezifizieren Namensräume, die nicht aufgebrochen werden können, und Sichtbarkeitsregeln für selbstdefinierte Variablen, Funktionen und andere Bezeichner.

Go besitzt einige Eigenschaften aus funktionalen Programmiersprachen wie First Class Functions (Funktionen, die als Parameter oder Rückgabewerte anderer Funktionen genutzt werden können) / High Order Functions (Funktionen, die andere Funktionen als Parameter oder Rückgabewert haben), Funktionen mit mehreren Rückgabewerten, variadische Funktionen, anonyme Funktionen und Closures. Eine richtige funktionale Sprache ist Go allerdings nicht, beispielsweise können Funktionen Seiteneffekte haben und es gibt keine strikte Call-by-Value Semantik.

Die Entwickler bezeichnen Go nicht als objektorientierte Programmiersprache, obwohl viele der Eigenschaften objektorientierter Sprachen vorhanden sind. Es gibt keine Klassen; stattdessen können Methoden für alle selbstdefinierten Variablentypen definiert werden. Eine Vererbungshierarchie gibt es nicht; in vielen Fällen können aber ähnliche Strukturen durch Komposition erzielt werden, d.h. ein Datentyp enthält einen anderen Typ und nutzt dessen Methoden. Eine Zugriffskontrolle auf Daten eines Objekts gibt es nicht; stattdessen können die Sichtbarkeitsregeln für Module als Zugriffsschutz verwendet werden.

Ein zentrales Konzept von Go sind Interfaces. Diese definieren eine Liste von Methoden, die ein Datentyp besitzen muss, um dem Interface zu entsprechen. Wenn das der Fall ist, können Werte eines konkreten Typs an Interface-Variablen zugewiesen werden. Damit ist Polymorphie möglich: einer Funktion, deren Parameter eine Interface-Variable ist, können beliebige Objekte übergeben werden, sofern der Typ das Interface erfüllt. Die Funktion kann die im Interface definierten Methoden direkt aufrufen.

Go unterstützt die nebenläufige bzw. parallele Programmierung durch Goroutinen. Diese orientieren sich am den Communicating Sequential Processes, die von Tony Hoare in den 70er Jahren definiert wurden. Eine Goroutine ist eine sehr leichtgewichtige, nebenläufige Ausführungseinheit. Sie benötigt initial lediglich ca. 2 kB Speicher, so dass auf aktuellen Systemen leicht eine Million und mehr Goroutinen gestartet werden können. Andere parallele Sprachen bilden dagegen Nebenläufigkeit oft auf Betriebssystem-Threads ab, so dass bereits einige tausend davon problematisch sind.

Schlüsselwörter und vordefinierte Bezeichner

Es gibt nur 25 Schlüsselwörter in Go. Diese definieren die grundlegende Syntax von Go Programmen und dürfen nicht als Namen für selbst definierte Variablen, Funktionen o.ä. verwendet werden. Die Schlüsselwörter sind

break        default      func         interface    select
case         defer        go           map          struct
chan         else         goto         package      switch
const        fallthrough  if           range        type
continue     for          import       return       var

In Go 1 werden der Sprache garantiert keine weiteren Schlüsselwörter hinzugefügt. Würde dies getan, dann stände das entsprechende Wort nicht mehr für eigene Definitionen zur Verfügung, und existierende Programme können ggf. nicht mehr compiliert werden.

Daneben gibt es 39 vordefinierte Bezeichner. Diese entsprechen syntaktisch einem Typ, einer Funktion oder einer Konstanten, die nicht definiert werden muss, sondern in allen Packages zur Verfügung steht. Dies sind:

Typen:

bool  byte   complex64  complex128  error   float32  float64
int   int8   int16      int32       int64   rune     string
uint  uint8  uint16     uint32      uint64  uintptr

Konstanten:

nil  true  false  iota

Funktionen:

append  cap  close  complex  copy     delete  imag     len
make    new  panic  print    println  real    recover

Die Namen der vordefinierten Bezeichner können jederzeit als Namen für eigene Variablen, Typen, Interfaces, Funktionen etc. verwendet werden. Wenn man das macht, haben die eigenen Definitionen Vorrang vor den vordefinierten. Die eingebauten Funktionen sind dann im jeweiligen Gültigkeitsbereich nicht mehr sichtbar. Somit können der Sprache weitere vordefinierte Bezeichner hinzugefügt werden, ohne dass die Gefahr besteht, dass ältere Programme nicht mehr compiliert werden können.

Typsicherheit und Garbage Collection

Go ist eine statisch typisierte Programmiersprache. Fehler bei der Verwendung von Typen werden bereits beim Compilieren bemerkt und können nicht zur Laufzeit ein Programm zum Absturz bringen. Konvertierungen zwischen Typen sind in gewissen Grenzen möglich, müssen aber im Gegensatz zu anderen Sprachen explizit ausgeschrieben werden, um Mehrdeutigkeiten und versehentliche Konvertierungen zu vermeiden.

Polymorphie kann durch Interfaces implementiert werden. Einer Interface-Variablen können Werte verschiedener Typen zugewiesen werden, sofern diese zum Interface passen, d.h. eine spezifizierte Menge an Methoden besitzen. Mit Hilfe von Typ Assertions können die ursprünglichen Datentypen zurück gewonnen werden, wobei hier Laufzeitfehler möglich sind, wenn bei der Programmierung nicht alle möglichen Fälle bedacht wurden.

Eine Besonderheit bildet das leere Interface interface{}, dem Werte beliebiger Typen zugewiesen werden können, auch der eingebauten Typen wie int oder string. Bei der Verwendung des leeren Interfaces verliert der Programmierer weitgehend die Typsicherheit und muss alle möglichen Fälle explizit prüfen.

In Go ist es nicht möglich, ein Objekt explizit zu löschen. Die Laufzeitumgebung erkennt selbstständig, wann ein Objekt nicht mehr verwendet werden kann (z.B. weil der Scope seiner Sichtbarkeit verlassen wurde und es keine Referenzen mehr darauf gibt), und löscht es mittels der automatischen Speicherbereinigung. Da das Löschen nachgelagert erfolgt, verbraucht ein Go-Programm oft mehr Speicher als ein vergleichbares C oder C++ Programm. Durch die Garbage Collection ist es aber ausgeschlossen, dass ein bereits gelöschtes Objekt nochmal referenziert wird und dadurch versehentlich andere Objekte unkontrolliert überschrieben werden oder sogar das Programm abstürzt.

Zusammengesetzte Datentypen

Neben den üblichen numerischen Datentypen, die in Compilersprachen üblich sind (Integer diverser Längen, signed und unsigned, Fließkomma) und Verbundtypen (Struct) bietet Go nur wenige eingebaute Datentypen, die aber sehr ausgefeilt sind und vor dem Hintergrund umfangreicher praktischer Erfahrung mit Softwareentwicklung entworfen wurden.

Ein Array ist eine Reihung mit fixer Anzahl von Elementen. Die Anzahl muss bereits zur Übersetzungszeit feststehen und ist Bestandteil des Typs. Damit sind Arrays zwar nicht sehr flexibel, aber da die Lage der Elemente im Speicher genau feststeht, sehr performant. Arrays kommen zum Beispiel in kryptographischer Software zum Einsatz, z.B. kann man eine MD5 Prüfsumme als Array von 16 Bytes darstellen.

Ein Slice ist ebenfalls eine Reihung von Elementen, aber mit variabler Anzahl, die sich zur Laufzeit ändern kann. Dabei wird zwischen Länge und Kapazität unterschieden; ersteres ist der indizierbare Bereich, zweiteres die Größe, auf die sich die Länge erhöhen lässt, ohne die Datenelemente umzukopieren. Operationen mit Slices sind zwar etwas langsamer als Arrays, dafür bieten sie eine Flexibilität, die sonst nur in dynamischen, interpretierten Sprachen vorhanden ist.

Ein weiterer eingebauter Datentyp ist das assoziative Array, das in Go Map heißt. Es können beliebige (eingebaute oder selbstdefinierte) Typen als Schlüssel und Wert verwendet werden.

Der eingebaute Iterator Range kann verwendet werden, um über alle Elemente von Array, Slices und Maps zu iterieren. Anders als in vielen Sprachen ist es dabei erlaubt, dass sich das Array, der Slice oder die Map während des Iterierens ändert. Das gilt sowohl für Schleifen, in denen Objekte geändert werden, als auch für nebenläufige Operationen, d.h. wenn Iteration und Änderung in verschiedenen Goroutinen stattfinden.

Speziell für die nebenläufige Programmierung gibt es den Datentyp Channel, der einen typsicheren Kommunikationskanal implementiert. Er wird zum Austausch von Nachrichten und zur Synchronisierung zwischen Goroutinen verwendet. Es gibt gepufferte und ungepufferte Kanäle; erstere besitzen eine einstellbare Kapazität zur Zwischenspeicherung von Nachrichten und unterstützen damit eine Pipeline-artige Datenverarbeitung. Ungepufferte Kanäle ermöglichen eine beidseitige Synchronisierung zwischen Goroutinen.

In der Standardbibliothek stehen weitere generische Datentypen (z.B. Heaps, doppelt verkettete Listen) und Kommunikationsprimitive (z.B. Mutex) zur Verfügung.