Wer Go noch nicht kennt, kann sich in diesem Blogpost einen Überblick verschaffen. Angeblich wurden die Grundlagen dieser Programmiersprache bei Google ausgetüftelt, während die Entwickler auf die Kompilierung größerer Anwendungen warten mussten. Go ist deswegen auch keine Sprache mit revolutionären Konzepten, sondern eine moderne, leicht handhabbare und vor allem schnell kompilierbare C-Alternative.
- Google veröffentlichte 2009 die Programmiersprache Go.
- Wichtige Entwurfsziele waren automatische Speicherverwaltung, schnelle Kompilierung und native Unterstützung für nebenläufige, systemnahe Programmierung.
- Die Vorteile einer interpretierten, dynamisch typisierten Sprache sollten mit der Effizienz und Sicherheit einer kompilierten, statisch typisierten Sprache kombiniert werden.
- Abhängigkeiten sollten direkt aus dem Quellcode ableitbar sein.
Ein erster Blick auf den Code zeigt, dass Go eine C-ähnliche Programmiersprache ist. Es gibt weniger Schleifen, weniger Variablendeklarationen und keine Semikolons:
Das Code-Snippet gehört zum Paket main
und importiert das Standardpaket fmt
. Innerhalb des Paketes main
sind die Funktionen add
und main
definiert. In diesem Paket beginnt standardmäßig die Ausführung des Programms. Falls man ein wiederverwendbares Paket entwickeln möchte, das nicht selbst ausführbar ist, würde man das Paket main
und die Funktion main
weglassen.
Ein Go-Programm besteht aus Paketen, die zusammengehörige Funktionen und Variablen gruppieren. Ein Paket entspricht üblicherweise einem Dateiverzeichnis gleichen Namens. Die Dateien im Verzeichnis mit der gleichen package <name>
Direktive bilden eine modulare Einheit. Denkbar wäre zum Beispiel das Paket blog
im gleichnamigen Verzeichnis mit den Dateien blog.go
, post.go
and comment.go
.
Gute Paketnamen können den Code ungemein verbessern. Der Paketname sollte einen passenden Kontext für seinen Inhalt bieten, sodass Benutzer diesen einfacher verstehen können. Auch die Entwickler eines Paketes können bei dessen Weiterentwicklung leichter entscheiden, was in das Paket gehört und was nicht. Gute Paketnamen wie time
, list
und http
sind kurz und verständlich. Typischerweise werden kleingeschriebene Substantive verwendet. Wortzusammensetzung mit camelCase, PascalCase oder Bindestrichen sind für Go-Pakete unüblich. Falls es keinen kurzen, passenden Begriff gibt, kann man Paketnamen auch abkürzen. Bekannte Beispiele sind fmt
, syscall
und strconv
.
Im obigen Code-Snippet wird die Funktion Println
aus dem Paket fmt
verwendet. Diese Wiederverwendung ist möglich, weil die Funktion von diesem Paket exportiert wurde. Generell werden alle großgeschriebenen Namen von einem Paket exportiert. Aus diesem Grund beginnt die Funktion Println
mit einem großen “P”. Kleingeschriebene Namen werden nicht exportiert und können von anderen Paketen nicht aufgerufen werden.
Tipp: Um die Modularisierung des Quellcodes zu verbessern, sollte die Sichtbarkeit möglichst gering sein. Aus diesem Grund sollten Variablen und Funktionen standardmäßig kleingeschrieben werden. Nur wenn tatsächlich Zugriff aus einem anderen Paket notwendig ist, sollten Namen exportiert werden.
Abhängigkeiten zwischen Paketen
Die Google-Mitarbeiter verfolgten von Anfang an das Ziel bei der Entwicklung von Go, Abhängigkeiten zwischen Paketen direkt im Quellcode ausdrücken zu können. So entstand die Idee der Pakete und der Import-Blöcke, wo Namen und Pfade angegeben werden können.
Mit Hilfe von Konventionen vermeiden Go-Entwickler in vielen Fällen Konfigurationen. Typischerweise reicht die Information im Quellcode aus, um ein Go-Programm zu bauen. Makefiles, Shell-Skripte etc. sind nicht notwendig.
Tipp: Go funktioniert ohne Konfiguration dank seiner etablierten Konventionen, die Einfluss auf Workspace-Struktur, Namen und Werkzeuge haben. Deshalb ist es wichtig, diese Konventionen zu kennen und zu befolgen.
Basistypen
Go bietet eine Reihe von eingebauten Basistypen:
bool
string
int
int8
… int64
uint
unit8
… uint64
uintptr
byte
rune
float32
float64
complex64
complex128
Anhand des Namens kann man sicherlich erkennen, welche Bedeutung diese Datentypen haben. Erklärungsbedürftig sind vermutlich die Basistypen uintptr
und rune
. Ersterer ist ein ausreichend großer Integer, der beliebige Zeiger beinhalten kann. Letzterer entspricht int32
und wird zur Unterscheidung von Zahlen und Textzeichen verwendet.
Bei Zuweisungen von Werten unterschiedlichen Typs muss immer eine explizite Typumwandlung erfolgen. Bei einer Typumwandlung wird der Wert v
in den Typ T
mit dem Ausdruck T(v)
umgewandelt:
Durch Typinferenz kann eine Variable auch ohne die Angabe eines Datentyps deklariert werden. Der Typ der Variable wird in diesem Fall aus dem Wert auf der rechten Seite abgeleitet.
Wertzuweisungen erfolgen mit dem =
Operator. Der :=
Operator ist genaugenommen kein Operator, sondern Teil einer verkürzten Schreibweise zur Deklaration von Variablen innerhalb von Funktionen.
Variablen
Eine Variable kann mit der Anweisung var
deklariert werden. Genauer gesagt wird mit var
sogar eine Liste von Variablen deklariert. Der Typ der Variablen steht an letzter Stelle. Die Anweisung var
kann auf Paket- und auf Funktionsebene genutzt werden.
Variablen, die ohne Intializer deklariert werden, haben trotzdem einen sogenannten Nullwert. Numerische Typen haben implizit den Wert 0, boolesche Werte sind false und Strings werden mit einem Leerstring ""
initialisiert. Der Nullwert von Maps und Arrays ist nil
. Auf nil
kommen wir später noch einmal zurück.
Variablen können bei ihrer Deklaration ebenfalls mit einem Wert versehen werden.
Die :=
Anweisung kann innerhalb einer Funktion benutzt werden, um etwas kompakter eine Variable zu deklarieren und mit einem Wert zu belegen. Außerhalb einer Funktion kann die :=
Anweisung nicht verwendet werden.
Konstanten können mit dem Schlüsselwort const deklariert werden. Die :=
Anweisung kann nicht für Konstanten verwendet werden.
Arrays und Slices
Arrays sind ein wichtiger Baustein von Go-Programmen. Ein Array mit der Länge n
und dem Typ T
wird mit der Anweisung [n]T
deklariert. Die Länge eines Arrays kann nicht verändert werden, denn sie ist ein Bestandteil des Typs.
Wie man sieht, hat der erste Eintrag des Arrays den Index 0 und der letzte den Index len(array)-1
. Alternativ kann man bei der Deklaration eines Arrays auch dessen initialen Inhalt angeben:
Nachvollziehbarerweise hat das Array eine Länge und Kapazität von 3. Warum Go zwischen Länge und Kapazität unterscheidet, wird schnell klar, wenn man sich die Slices anschaut. Slices sind eine weitere wichtige Datenstruktur. Ein Slice ist kein Array, sondern eine Sicht auf einen darunterliegenden Teil eines Arrays, ohne eigenen Speicherplatz für Einträge. Im übertragenen Sinn erfüllt ein Slice die Funktion einer Liste. Der folgende Slice zeigt auf eine zusammenliegende Sektion des Arrays mit den Elementen 0 und 1.
Die Länge eines Arrays kann nicht verändert werden. Es ist jedoch möglich, einen neuen Slice für ein Array zu erzeugen, das beispielsweise um das letzte Element verkürzt ist:
Ein Slice hat eine Länge und eine Kapazität. Die Länge entspricht der Anzahl der Elemente, die es enthält. Die Kapazität bezieht sich auf die Anzahl der Elemente im darunterliegenden Array.
Der Null-Wert eines Slice ist nil
. Ein nil-Slice hat kein darunterliegendes Array, weswegen Kapazität und Länge 0 sind.
Mit der Standardfunktion append
können Elemente zu einem Slice bzw. Array hinzugefügt werden.
Maps
Eine weitere wichtige Datenstruktur sind Maps, die Schlüsseln Werte zuordnen. Maps werden mit dem Ausdruck make
erzeugt.
Funktionen
Innerhalb eines Paketes können Funktionen definiert werden. Die Funktion add
im folgenden Beispiel gibt einen Integer als Anzahl der Fahrzeuge zurück. Die Funktion get
gibt einen Integer und einen booleschen Wert zurück, sodass man zwischen Anzahl 0 und Nichtvorhandensein unterscheiden kann. Das Beispiel zeigt außerdem, dass Wertzuweisungen und Deklarationen auch für mehr als eine Variable möglich sind.
Go bietet keine automatische Unterstützung für Getter und Setter. Es spricht aber nichts dagegen, diese programmatisch hinzuzufügen. Es gibt keine allgemeine Regel zur Präfixbenutzung von “Get” oder “Set” im Namen. Falls man einen öffentlichen Getter für ein Feld firstname
hinzufügen möchte, sollte man GetFirstname
oder Firstname
als Namen verwenden.
In jedem Paket kann wahlweise eine init
Funktion genutzt werden, die nach der Initialisierung der Variablen ausgeführt wird.
In diesem Beispiel wird die Funktion configureApplication
zur Initialisierung der Variablen ConfigSuccess
ausgeführt. Die Ausführung von init
folgt im Anschluss. Erst am Ende wird die Funktion main
aufgerufen.
First-Class-Funktionen sind ein großer Vorteil von Go. In diesem Fall kann man eine Funktion als Wert betrachten. Eine Funktion kann deswegen einer anderen Funktion als Argument übergeben werden. Man kann eine Funktion auch einer Variablen zuordnen oder sie als Rückgabewert nutzen. Go unterstützt anonyme Funktionen, mit denen Closures gebildet werden können.
Verzweigungsstrukturen
Die for-Schleife funktioniert wie in Java oder C. Die runden Klammern fehlen, dafür sind die geschweiften nicht optional. Go unterstützt ebenfalls die aus anderen Programmiersprachen bekannten Anweisungen break
und continue
. Weil man Start- und Zählschritt weglassen kann, könnte eine for-Schleife auch als while-Schleife geutzt werden.
Ein for-Schleife kann in Kombination mit dem Ausdruck range benutzt werden, um über ein Slice oder eine Map zu iterieren:
Die if- und else-Anweisungen sehen bis auf die fehlenden Klammern wie bei Java oder C aus. Wie bei der for-Schleife kann man optional eine Anweisung vor der Bedingung ausführen.
Go bietet ebenfalls ein switch-Statement mit den Schlüsselwörtern switch
, case
, default
und fallthrough
. Switch-Anweisungen werten die angegebenen Cases von oben nach unten aus. Sobald ein Case zutrifft, wird abgebochen. Nur durch explizite Angabe von fallthrough
wird mit dem folgenden Case fortgefahren.
Strukturen und Interfaces
Ein Verbund von Elementen kann mit struct
erzeugt werden. Es handelt sich hierbei um eine Typdeklaration. Es ist möglich, Strukturen ineinander zu schachteln, um komplexere Modelle abzubilden.
Go verzichtet auf Klassen zur Umsetzung von Objektorientierung, stattdessen gibt es Interfaces. Ein Interface ist eine Liste von Funktionen. In diesem Beispiel hat das Interface Shape
eine Funktion zur Berechnung der Fläche:
make und new
Go kennt die zwei eingebauten Funktionen new
und make
zur Speicherreservierung. Mit new
wird Speicherplatz für Werte wie int
, Address
oder Person
allokiert und “nullisiert”. Die Rückgabe von new(T)
ist ein Pointer vom Typ T
.
Anschließend kann der “nullisierte” Speicher ohne weitere Initialisierung benutzt werden. Die Dokumentation von bytes.Buffer
besagt zum Beispiel, dass der Nullwert von Buffer
ein leerer, einsetzbarer Puffer ist.
Channels, Slices und Maps werden hingegen mit make
erzeugt. Die Funktion make
allokiert und initialisiert Speicher für diese Datenstrukturen. Die Rückgabe von make(T)
ist kein Zeiger, sondern ein initialisierter Wert von Typ T
.
Go-Entwickler müssen nicht wissen, wo im Speicher die Variablen abgelegt sind. Der Go-Compiler entscheidet, ob eine Variable auf dem Stack oder auf dem Heap liegt. Eine Variable existiert solang es eine Referenz darauf gibt. Die Garbage Collection funktioniert automatisch.
Zeiger
Im Gegensatz zu Java bietet Go auch die Möglichkeit, mit Zeigern zu arbeiten. Ein Zeiger hält die Adresse einer Variablen im Speicher. Im folgenden Snippet wird eine Variable p
deklariert deren Typ ein Zeiger auf einen Wert des Typs string
ist:
Mihilfe des &
Operators wird ein Zeiger auf seinen Operanden erzeugt:
Das Dereferenzieren des Pointers erfolgt mit dem *
Operator. Der Ausdruck *p
liefert den Wert, auf den der Pointer verweist:
Verzögerte Funktionsaufrufe
Funktionsaufrufe können mit der Anweisung defer verzögert ausgeführt werden. Die markierten Funktionen werden auf einen Stack gelegt und nach der Rückkehr der umgebenden Funktion in Last-In-First-Out-Reihenfolge ausgeführt. Dieses Feature kann man beispielsweise einsetzen, um Aufräumarbeiten durchzuführen.
Nennenswert sind ebenfalls die Standardfunktionen panic
und recover
. Sobald panic
aufgerufen wird, stoppt die Ausführung einer Funktion und die verzögerten Funktionen werden der Reihe nach gestartet. Innerhalb der verzögerten Funktionen kann recover
genutzt werden, um zur normalen Ausführung zurückzukehren.
Ausnahmen und try-catch-finally-Muster, wie man sie beispielsweise aus Java kennt, werden in Go nicht eingesetzt.
Objektorientierung
Go wurde in diesem Blogpost als moderne C-Alternative vorgestellt. Die ursprünglich für systemnahe Entwicklung konzipierte Programmiersprache kann prinzipiell überall eingesetzt werden. Wer eine Sprache wie Go lernen möchte, wird früher oder später verstehen wollen, wie Objektorientierung mit dieser Sprache funktioniert. Mit dieser Fragestellung beschäftigt sich der nächste Blogpost über Go.