Original-URL des Artikels: https://www.golem.de/news/programmiersprache-go-schlanke-syntax-schneller-compiler-2008-149533.html    Veröffentlicht: 03.08.2020 12:03    Kurz-URL: https://glm.io/149533

Programmiersprache Go

Schlanke Syntax, schneller Compiler

Die objektorientierte Programmiersprache Go eignet sich vor allem zum Schreiben von Netzwerk- und Cloud-Diensten.

Weil ihnen die etablierten Programmiersprachen zu große Schwächen aufwiesen, starteten die drei Google-Mitarbeiter Robert Griesemer, Rob Pike und Ken Thompson 2007 die Arbeit an einer neuen Programmiersprache. C, C++ und Java waren ihnen zu komplex, liefen zu langsam oder besaßen bummelnde Compiler.

Das Ergebnis namens Go kommt mit einer deutlich schlankeren Syntax aus, unterstützt von Haus aus Nebenläufigkeit, arbeitet mit Unicode und kompiliert besonders flott seinen Quellcode zu nativen Binärprogrammen.

Den Speicher räumt im Hintergrund eine intelligente Garbage Collection auf.

Mittlerweile kommt Go in zahlreichen größeren Projekten zum Einsatz. Dazu zählen Docker, die Datenbank InfluxDB, OpenShift und Kubernetes. Alleine letztgenannte Software zählt bereits über eine Million Codezeilen.

Das niedliche Maskottchen ist übrigens kein Hamster, sondern eine Taschenratte (englisch: Gopher).

Kompakte Syntax

Wer schon einmal in C, C++ oder Java programmiert hat, dürfte in Go einiges wiedererkennen, mit vielem aber auch fremdeln. So muss in Go am Ende von Anweisungen kein Semikolon stehen.

Folgendes Beispiel berechnet den größten gemeinsamen Teiler:

package main import "fmt" // Berechne ggT func ggT(a int32, b int32) int32 { if b == 0 { return a } if a == 0 { return b } for b != 0 { temp := a % b a = b b = temp } return a } func main() { fmt.Println(ggT(30,50)) }

Zunächst holt import das Paket fmt aus der Standardbibliothek hinzu. Es enthält unter anderem die Funktion fmt.Println(), die Texte ausgibt. Anschließend definiert das Beispiel mit dem Schlüsselwort func eine neue Funktion namens ggT, die zwei Zahlen a und b entgegennimmt und den größten gemeinsamen Teiler als Integerzahl zurückliefert. Wie in C++ und Java rahmen die geschweiften Klammern alle zur Funktion gehörenden Anweisungen ein. Funktionen lassen sich übrigens als Werte an andere Funktionen übergeben, zudem unterstützt Go Closures.

Typendeklaration

In Go muss bei der Deklaration von Variablen immer ein Typ angegeben werden - und zwar hinter dem Variablennamen. int32 aus dem Beispiel steht dabei für eine vorzeichenbehaftete 32-Bit-Ganzzahl. Es gibt auch ein int, dessen Größe jedoch von der entsprechenden Prozessorarchitektur abhängt und somit zwischen den Systemen variieren kann. In Funktionen kann Go den Datentyp über den Operator := auch selbst ermitteln. Beim größten gemeinsamen Teiler leitet Go so den Typ der Variablen temp ab.

Gibt eine Funktion wie ggT() einen Wert zurück, steht sein Typ hinter dem Funktionsnamen. Funktionen dürfen zudem mehrere Werte zurückliefern. Das machen sich viele Funktionen zunutze, um einen Fehler zu melden: Go selbst kennt keinen Mechanismus zur Fehlerbehandlung, wie dies andere Sprachen mit Exceptions oder throw/catch-Konstrukten bereitstellen. Die meisten Go-Funktionen zeigen daher in einem zusätzlichen Rückgabewert an, ob ein Fehler aufgetreten ist. Dieser Wert muss anschließend lediglich per if-Abfrage getestet werden.

Auch bei Arrays steht der Typ hinter den eckigen Klammern, was zu folgenden Konstrukten führt:

ampel := [3]string{"Rot", "Gelb", "Grün"}

In ampel[2] würde anschließend der Text "Grün" liegen. Ergänzend gibt es noch Maps und Slices. Letztgenannte sind dynamische Arrays, die sich verändern, verlängern und kürzen lassen. Maps entsprechen den Dictionaries aus anderen Sprachen und funktionieren ähnlich wie ein Telefonbuch. Go kennt derzeit nur die aus C, C++ und Java bekannte for-Schleife, die im folgenden Beispiel i von 0 um eins hoch zählt, solange i kleiner als 10 ist:

for i := 0; i < 10; i++ {...}

Übergibt man wie beim größten gemeinsamen Teiler nur eine Bedingung wie b > 0, läuft die Schleife, bis die Bedingung erfüllt ist. for ersetzt so die in anderen Sprachen bekannte while-Schleife. Ergänzend kennt Go noch die nützliche Variante for i, r := range ampel {...}. Diese Schleife zählt i bei jedem Durchlauf um eins hoch, r enthält das jeweils nächste Element aus dem Array ampel. Sofern man i nicht benötigt, weist man darauf mit einem Unterstrich hin:

for _, r := range ampel {...}



Go hat Zeiger

Aus C hat Go das Konzept der Zeiger übernommen. Diese speziellen Variablen merken sich die Speicheradresse eines Wertes. Übergibt man einer Funktion einen solchen Zeiger (Call by Reference), kann sie direkt den an dieser Speicheradresse abgelegten Wert ändern.

Im unten stehenden Beispiel tauscht die Funktion swap() die Inhalte der Variablen a und b. Die Funktion bekommt zwei Zeiger übergeben, die jeweils auf einen Ganzzahlwert zeigen. In der Funktion steht dann *p1 für den Wert, auf den p1 zeigt.

Das & liefert einen Zeiger auf den Inhalt der entsprechenden Variablen. Anders als C bietet Go allerdings keine Zeigerarithmetik an.

var a int = 1 var b int = 2 func swap(p1 *int, p2 *int) { var temp int temp = *p1 *p1 = *p2 *p2 = temp } swap(&a, &b)

Klassenlos objektiv

Go ist zwar eine objektorientierte Sprache, verzichtet jedoch auf Klassen und Vererbung. Stattdessen existieren nur Structs, die mehrere Werte zusammenfassen. Ein Punkt besteht beispielsweise aus zwei Integer-Werten:

type Point struct { x int y int } var p Point = Point { 1,2 }

Die Klammernotation erzeugt einen konkreten Point, an dessen Elemente man anschließend über die Punktnotation p.x gelangt. Methoden kann man den Structs einfach nach Belieben hinzufügen:

func (p Point) Print() { fmt.Printf("%d, %d\n", p.x, p.y) }

Die Angabe in den Klammern hinter dem Schlüsselwort func verrät, zu welchem Datentyp die jeweilige Methode gehört. Go bezeichnet diese Angabe als Receiver Argument. Innerhalb der Funktion sind das Struct und seine Werte dann unter dem angegebenen Namen verfügbar - im Beispiel etwa der Punkt unter dem Namen p. Diese Notation ohne Klassen hat den Vorteil, dass auch nachträglich einfach eine Methode auf eine Struct implementiert werden kann.

Im obigen Beispiel gibt übrigens fmt.Printf() die im Punkt gespeicherten Werte aus. Die Syntax orientiert sich dabei an printf() aus C: Anstelle der Platzhalter %d baut die Funktion die Werte von p.x und p.y ein.

Interfaces

Polymorphismus erreicht Go über Interfaces, wie sie unter anderem auch Java kennt. Interfaces geben eine Liste mit Methoden vor, die dann für ausgewählte Structs implementiert werden:

type Debug interface { Print() }

Bietet eine Struct die Methode Print() an, erfüllt sie automatisch das Interface Debug. Damit lässt sich eine Variable für das Interface anlegen. Die kann dann wiederum alle Objekte speichern, die das Interface implementieren. Im Beispiel erfüllt Point das Interface Debug:

func main() { var a Debug p := Point{ 1,2,3 } // p implementiert das Debug-Interface: a = p a.Print() }

Nebenläufigkeit

Stellt man einer Funktion das Schlüsselwort go voran, führt sie Go automatisch nebenläufig aus:

go berechnewas()

Diese sogenannten Goroutinen kommunizieren über Kanäle (Channels) miteinander, die wie Warteschlangen funktionieren. Einen neuen Channel muss man vor seiner Benutzung erstellen und dabei den Typ der über den Kanal ausgetauschten Daten festlegen. Im folgenden Beispiel könnten die Goroutinen über den Channel Integerzahlen austauschen:

kanal := make(chan int, 100)

Die Zahl gibt die Größe der Warteschlange an. Bei einer 0 entsteht ein synchroner Channel. Dieser blockiert eine Goroutine so lange, bis eine andere aus dem Kanal liest oder schreibt. Eine Goroutine kann mehrere Channels gleichzeitig nutzen. Für den Zugriff auf einen Channel existiert die Notation:

kanal <- 25 ausgabe := <- kanal

Der erste Befehl schiebt die 25 in den Channel, der zweite holt die 25 wieder heraus und speichert sie in der Variablen ausgabe. Die Goroutinen und das Channel-System verhindern allerdings keine Race Conditions. Zwei Goroutinen könnten sich folglich gegenseitig blockieren. Seit der Go-Version 1.14 lassen sich Goroutinen jedoch asynchron unterbrechen.



Quellcode kann als Packages gruppiert werden

Der Go-Quellcode wird über mehrere Dateien verteilt, die sich wiederum jeweils in sogenannten Packages gruppieren lassen. Jede Quellcodedatei gehört immer genau einem Package an. Existieren muss mindestens ein Package mit dem Namen main. Beim Start des kompilierten Programms ruft Go in diesem Paket automatisch die Funktion main() auf - ähnlich wie bei C und C++.

In einer Quellcodedatei bindet man Pakete über die import-Anweisung ein und kann dann die im Package definierten Funktionen im eigenen Code nutzen. Dabei lauert allerdings gerade beim Umstieg eine kleine Stolperfalle: Nur wenn der Name einer Funktion mit einem Großbuchstaben beginnt, lässt sie sich in einem beliebigen anderen Package nutzen.

Schreibt man den Namen hingegen klein, dürfen nur noch die Methoden aus dem eigenen Package darauf zugreifen. Aus diesem Grund lässt sich fmt.Println() in jeder Quellcodedatei aufrufen - vorausgesetzt man hat dort das Paket fmt importiert. Die Groß- und Kleinschreibung ersetzt in Go somit die aus anderen Sprachen bekannten Zugriffsmodifikatoren private und public.

Go bringt bereits von Haus aus ein paar Packages mit. Sie offerieren neben Konstanten und mathematischen Formeln unter anderem auch Funktionen zum Zugriff auf das Dateisystem, zur Verarbeitung von XML- und JSON-Daten, zur Abwicklung von HTTP-Kommunikation und zur Verschlüsselung von Daten.

Modular

Ursprünglich verstaute man seine Pakete in einem Verzeichnis und verriet dem Compiler den Speicherort über die Umgebungsvariable GOPATH. Dieses simple Konzept führte jedoch schnell zu Abhängigkeitsproblemen und schließlich zur Entwicklung eines neuen Modulsystems. Das liegt mit der Veröffentlichung von Go 1.14 im Februar 2020 endlich in einer stabilen Fassung vor.

Ein Modul bündelt dabei mehrere zusammengehörende Packages. Es liegt entweder in einem eigenen Verzeichnis auf der Festplatte oder auf einem externen Server in einem Versionskontrollsystem. In der Import-Anweisung gibt man lediglich den passenden Ort an:

import "github.com/spf13/cobra"

Github hat sich dabei zu einem beliebten Ablageort für Module entwickelt und wurde so zu einem inoffiziellen Repository für Go-Pakete.

Compiler-Spezialitäten

In jedem Fall kümmert sich der Compiler bei der Übersetzung um das Modulmanagement, lädt die notwendigen Packages herunter und löst Abhängigkeiten automatisch auf. Anders als etwa bei Python mit PIP benötigt Go keinen eigenen Paketmanager.

Der Compiler ist selbst in Go geschrieben und kann das fertige Binary direkt für verschiedene Architekturen und Betriebssysteme generieren. Dazu gehören unter anderem auch ARM-Systeme wie der Raspberry Pi. Dieses Cross-Compiling funktioniert ohne die Installation von weiteren Abhängigkeiten, es muss nur die gewünschte Plattform angegeben werden. Das erzeugte Ergebnis ist in jedem Fall ein statisch gelinktes Programm, das ohne Abhängigkeiten zu Bibliotheken auskommt. Eingebaut hat der Go-Compiler zudem ein kleines Test-Framework für Unit-Tests.



Im Herbst 2021 soll Go generische Datentypen erhalten

Derzeit erscheint im Halbjahresrhythmus eine neue Go-Version. Mit ihr kommen in der Regel neue Funktionen und mitunter auch Sprachkonstrukte hinzu. Innerhalb der Versionslinie 1.x sollen neue Go-Releases allerdings abwärtskompatibel bleiben. Älterer Quellcode lässt sich folglich mit neueren Go-Versionen ohne Änderungen übersetzen.

Im August 2020 steht die Version 1.16 an, die keine Änderungen an der Sprache, dafür aber zahlreiche kleinere an den Werkzeugen mitbringt. So soll der Linker deutlich weniger Ressourcen beanspruchen, das neue Paket time/tzdata erleichtert zudem den Zugriff auf die Zeitzonendatenbank.

Die nächste größere Neuerung ist für den Herbst 2021 geplant. Dann sollen generische Datentypen Einzug halten, die in anderen Programmiersprachen als Generica, parametrisierte Typen oder Templates bekannt sind. Die genaue Syntax und Funktionsweise steht noch nicht fest, ein erster Entwurf steht jedoch zur Diskussion.

Fazit

Go begeistert mit einer einfachen Syntax, der eingebauten Nebenläufigkeit und der bereits vorhandenen Unterstützung zur Netzwerkkommunikation. Es eignet sich daher vor allem für Server- und Cloud-Software. Wer von einer anderen objektorientierten Sprache mit Klassen umsteigt, muss sich allerdings etwas umgewöhnen.

Wer Go etwas näher kennenlernen möchte, sollte das interaktive Tutorial auf der Go-Webseite mitmachen. Ausprobieren lässt sich die Sprache zudem direkt im Browser im Go Playground. Dort warten auch direkt ein paar kleine Beispielprogramme.

 (tsh)


Verwandte Artikel:
Programmiersprache: Go 1.14 erscheint mit stabilem Modulsystem   
(26.02.2020, https://glm.io/146876 )
Sichere Programmiersprache: Microsoft will Rust statt Go auch in der Cloud   
(04.05.2020, https://glm.io/148235 )
Java: JDK 15 geht mit neuen Features in die General Availability   
(15.09.2020, https://glm.io/150865 )
UI-Framework: Google bringt Flutter auf Windows   
(24.09.2020, https://glm.io/151085 )
Bytecode-Optimierer: Kleinere, schnellere Android-Apps dank Facebook   
(13.04.2016, https://glm.io/120300 )

© 1997–2020 Golem.de, https://www.golem.de/