Go (Programmiersprache)

aus Wikipedia, der freien Enzyklopädie
Go
Go-Logo, 26. April 2018
Basisdaten
Paradigmen: nebenläufig, imperativ, strukturiert, modular, objektorientiert, generisch
Erscheinungsjahr: 2009; erste stabile Version 2012
Entwickler: Robert Griesemer, Rob Pike, Ken Thompson u. a.
Typisierung: stark, statisch
Wichtige Implementierungen: Gc, gccgo
Beeinflusst von: C, Newsqueak, Alef, Limbo, Oberon
Betriebssystem: Linux, macOS, FreeBSD, Windows, Fuchsia, Experimentell: DragonFly BSD, Plan 9, Solaris, z/OS
go.dev

Go ist eine kompilierbare Programmiersprache, die Nebenläufigkeit und automatische Speicherbereinigung unterstützt. Entwickelt wurde Go von Mitarbeitern des Unternehmens Google Inc.[1] Die Entwürfe stammen von Robert Griesemer, Rob Pike und Ken Thompson.

Überblick

Go wurde aus Unzufriedenheit über die bestehenden Sprachen zur Softwareentwicklung wie C++ oder Java im Kontext heutiger Computersysteme, insbesondere im Hinblick auf skalierbare Netzwerkdienste, Cluster- und Cloud Computing, entwickelt.[2] Im Vergleich zu C++ hat Go weit weniger Keywords. Eines der Probleme, das Go lösen möchte, ist die Compiler-Ineffizienz in C und C++. Wichtige Ziele bei der Entwicklung waren unter anderem die Unterstützung von Nebenläufigkeit mit nativen Sprachelementen und die Erleichterung der Softwareentwicklung mit großen Entwicklerteams und großen Codebasen.[3] Go besitzt einen eigenen Garbage Collector, erlaubt die Verwendung von Zeigern, verzichtet jedoch auf Zeigerarithmetik.[4] Go ist eine kompilierte Sprache, bei der Wert auf eine hohe Übersetzungsgeschwindigkeit gelegt wurde.

Go orientiert sich syntaktisch an der Programmiersprache C mit einigem Einfluss aus der Wirthschen Sprachfamilie (Pascal, Modula und insbesondere Oberon). Die Unterstützung für Nebenläufigkeit wurde nach Vorbild der von Tony Hoare eingeführten Communicating Sequential Processes (CSP) gestaltet und steht in Tradition der Programmiersprachen Newsqueak, Alef und Limbo.[5]

Merkmale und Sprachmittel

Go bietet Closures und Reflexion[6] sowie Typsicherheit und eine automatische Speicherbereinigung. Objektorientierung unterstützt Go durch Interfaces und Mixins. Auf Klassen und Vererbung von Klassen wird bewusst verzichtet. Außerdem ist es möglich, den Quellcode wie bei Java durch Pakete zu modularisieren.

Nebenläufigkeit wird durch Communicating Sequential Processes realisiert, die Goroutinen genannt werden und über Kanäle (Channels) miteinander kommunizieren können. Generische Typen wurden in Version 1.18 eingeführt.[7]

Unicode wird in Form von UTF-8 unterstützt, sowohl für Strings als auch für Variablenbezeichner im Quelltext (allerdings nur Unicode-Buchstaben und -Ziffern), Δt = t2 - t1 ist also möglich.[8]

Syntax

Die Syntax von Go orientiert sich im Wesentlichen an der Syntax der Programmiersprache C, weicht davon aber an einigen Stellen ab. So kann beispielsweise auf den Abschluss von Anweisungen durch ein Semikolon verzichtet werden. Datentypen werden bei Deklarationen hinter den Bezeichner geschrieben statt davor, um die Deklaration von Funktionstypen zu vereinfachen.[9] Code-Blöcke werden mit geschweiften Klammern abgegrenzt. Neben dem einfachen Gleichheitszeichen als Zuweisungsoperator gibt es zusätzlich den Operator :=, der Deklaration mit Typinferenz und Zuweisung kombiniert. Die Sprache umfasst mit 25 Schlüsselwörtern weniger Schlüsselwörter als ANSI C.

Kommentare werden wie in C oder C++ mit Schrägstrichen markiert; /* bis */ bezeichnet einen Kommentar, der auch mehrere Zeilen enthalten kann, // leitet einen Kommentar bis zum Ende der Zeile ein.

Jede Quelldatei gehört genau einem Paket an, das am Anfang der Datei mit der package-Anweisung angegeben wird.

Das Schlüsselwort für Funktionen lautet func, die Funktion main in dem „main“-Paket ist der Startpunkt des Go-Programms. Funktionen können mehrere Werte zurückgeben. Es ist üblich, als letzten Rückgabewert den Status über den Erfolg oder Misserfolg des Funktionsaufrufs zu übermitteln und sogleich mit einer bedingten Kontrollstruktur zu überprüfen.

Jede Variable hat einen definierten Typ. Jede Variable, mit Ausnahme des „Blank identifier“ _, muss verwendet werden. Der „Blank identifier“ ignoriert eine Zuweisung, es ist ein anonymer Platzhalter.

Die Prüfung einer Bedingung in einer Kontrollstruktur wie if, for oder switch wird anders als bei anderen Sprachen nicht von Klammern umschlossen.

Einfache Beispiele

package main

import "fmt"

func main() {
    fmt.Println("Hallo Welt")
}

Der obige Quelltext gibt am Ausgabemedium den String Hallo Welt aus.

Ein weiteres Beispiel berechnet die Kreiszahl Pi näherungsweise über die Leibniz-Reihe. Für die Berechnung werden nichtsequentielle Go-Routinen und ein Kanal verwendet:

package main

import (
    "fmt"
    "math"
)

func main() {
    fmt.Println(calcpi(5000))
}

// calcpi startet n Goroutinen, um eine
// Näherung von Pi zu berechnen.
func calcpi(n int) float64 {
    ch := make(chan float64, n)
    for k := 0; k < n; k++ {
        // alle n Werte nichtsequentiell berechnen
        go calcsubterm(ch, float64(k))
    }
    // das Ergebnis mit Null initialisieren
    f := float64(0.0)
    for k := 0; k < n; k++ {
        // alle n Werte addieren
        f += <- ch
    }
    return f
}

func calcsubterm(ch chan <- float64, k float64) {
    ch <- 4 * math.Pow(-1, k) / (2*k + 1)
}

Die einzelnen Summanden der mathematischen Reihe werden nichtsequentiell ausgeführt und schreiben ihre Ergebnisse jeweils in den Kanal ch. Gleichzeitig werden die Werte aus dem Kanal zu einem Gesamtergebnis addiert. Am Ausgabegerät erscheint die Ziffernfolge 3.141392653591793[10]. Die Abweichung zur eigentlichen Kreiszahl ab der vierten Nachkommastelle ist vor allem auf die langsame Konvergenz der Leibniz-Reihe zurückzuführen.

Typkonvertierung

Anders als bei der Sprache C müssen Typen immer konvertiert werden. Es ist zum Beispiel nicht möglich, einen Wert vom Typ Integer in einer Variable vom Typ Float zu speichern, ohne den Wert vorher zu konvertieren. Im folgenden Beispiel wird eine Variable vom Typ Integer deklariert und der Wert dieser Variable einer anderen Variable vom Typ Float zugewiesen. Man beachte, dass zwischen „int“ und „uint“ unterschieden wird. „Unsigned Integer“ „uint8“ umfassen einen Bereich von (0 … 255) während „int8“ einen Bereich von (−128 … 127) umfassen. Diese Vorgehensweise der strikten Typisierung ist sehr sinnvoll, da schwer zu findende Programmierfehler leichter erkannt werden können.

var int_variable1 int = 100
var float_variable1 float64 = float64(int_variable1)
var int_variable2 uint = uint(float_variable1)

Sofern bei der Deklaration direkt ein Wert zugewiesen wird, ist die explizite Nennung des Typs optional. Der Compiler ermittelt ihn anhand des Wertes oder der angegebenen Typenkonverterfunktionen. Auf Paketebene muss jede Anweisung mit einem Schlüsselwort beginnen, deshalb kann die folgende Kurzform nur innerhalb von Funktionen verwendet werden:

int_variable1 := 100
float_variable1 := float64(int_variable1)
int_variable2 := uint(float_variable1)

Zeiger

Im Gegensatz zu vielen Hochsprachen wie Java arbeitet die Programmiersprache Go mit Zeigern. Ein Zeiger (Englisch pointer) enthält als Wert die Adresse eines Speicherbereichs. Bestimmte Aufgaben können ohne Zeiger nicht programmiert werden. Dazu gehört die Übergabe von Werten an Funktionen (Call by reference). Im folgenden Beispiel wird eine Variable vom Typ „Integer“ mit dem Namen „nummer“ und dem Wert 100 initialisiert. Anschließend wird die Speicheradresse, also die Adresse im Speicher des Computers ausgegeben. Die Adresse der Variable „nummer“ wird also mit „&nummer“ abgerufen. Der Syntax der Zeiger erinnert stark an die Sprache C.

var nummer int = 100
fmt.Printf("Die Adresse der Variable: %x\n", &nummer)

Im nächsten Beispiel wird wieder eine Variable vom Typ „Integer“ mit dem Namen „nummer“ und dem Wert 100 initialisiert. Dann wird eine Pointer-Variable vom Typ Integer-Zeiger deklariert. Der Typ einer Zeiger Variable wird durch einen führenden Asterisk (*) vor dem Variablentyp deklariert. Aus „int“ wird „*int“. Anschließend wird die Speicheradresse der Variable „nummer“ als Wert der Zeiger Variable „integer_pointer“ deklariert. Daraufhin wird die Adresse des Speichers ausgegeben welche von der Variable „nummer“ belegt wurde. Zuletzt wird der Wert ausgegeben, welcher sich in der Speicheradresse befindet. Der Wert einer Speicherstelle, welcher in der Zeiger Variable „integer_pointer“ deklariert ist kann mit „*integer_pointer“ ermittelt werden.

var nummer int = 100
var integer_pointer *int
integer_pointer = &nummer
fmt.Printf("Adresse gespeichert in der integer_pointer Variable: %x\n", integer_pointer) /* gibt eine Speicheradresse aus z.B. 10888000 */
fmt.Printf("Wert der Speicheradresse gespeichert in integer_pointer : %d\n", *integer_pointer) /* Gibt den Wert an welcher in der Speicheradresse steht */

Eine Zeiger-Adresse ohne gespeicherten Zeiger wird „nil pointer“ genannt. Man kann einfach abfragen, ob eine Zeigeradresse eine Speicheradresse als Inhalt hat oder nicht.

if(integer_pointer != nil)    /* Wenn zutreffend Zeiger Variable speichert einen Zeiger auf einen Speicherbereich */
if(integer_pointer == nil)    /* Wenn zutreffend Zeiger Variable speichert keinen Zeiger auf einen Speicherbereich */

Im letzten Beispiel soll die Verwendung beim Aufruf einer Funktion dargestellt werden. Zuerst werden zwei Variablen vom Typ „Integer“ deklariert. Der Inhalt dieser zwei Variablen soll getauscht werden.

var nummer1 int = 10
var nummer2 int = 50
// Anzeige vor dem Tausch
fmt.Printf("Wert der Variable nummer1: %x\n", nummer1)
fmt.Printf("Wert der Variable nummer2: %x\n", nummer2)
// Aufruf der Funktion tauschen
tauschen(&nummer1,&nummer2)
// Anzeige des Tausches
fmt.Printf("Wert der Variable nummer1: %x\n", nummer1)
fmt.Printf("Wert der Variable nummer2: %x\n", nummer2)
//Funktion tauschen
func tauschen(nummer1_pointer *int, nummer2_pointer *int) {
    var zwischenspeicher int
    zwischenspeicher = *nummer1_pointer
    *nummer1_pointer = *nummer2_pointer
    *nummer2_pointer = zwischenspeicher
}

Man kann also grob zusammenfassen, dass „&“ die Speicheradresse einer Variable ermittelt, während „*“ den gespeicherten Wert einer Speicheradresse ermittelt.[11]

Objektorientierung

Go unterstützt objektorientierte Programmierung, sie ist jedoch nicht klassenbasiert. Datentypen können in Go Methoden besitzen. Polymorphie wird über Interfaces (Schnittstellen) erreicht, über die Methodenaufrufe zur Laufzeit an die konkrete Implementierung gebunden werden (Dynamische Bindung). Für einen Datentyp muss nicht explizit deklariert werden, dass er ein bestimmtes Interface erfüllt. Diese Beziehung wird stattdessen implizit beim Kompilieren ermittelt, um lose Kopplung zu erreichen.

Statt Vererbung und Typ-Hierarchien kommt in Go Komposition zum Einsatz. Hierfür unterstützt Go eine Form von Mixins, die in Go embedding („Einbettung“) genannt wird: eine Datenstruktur kann beliebig viele andere Datentypen einbetten, so dass sie deren Methoden und Datenfelder erhält.

Beispiel zu Typen, Interfaces und Mixins:

package main

import "fmt"

// Definieren zweier Typen
type User struct {
	Name string
}

type Admin struct {
	User // Admin bettet zudem den Typ 'User' ein
	Email string
}

// Ein Interface mit der Methode 'Notify()'
type Notifier interface {
	Notify()
}

// User und Admin implementieren das Interface 'Notifier'
// Eine vorherige Deklaration zur Implementierung ist nicht notwendig
func (u User) Notify() {
	fmt.Printf("User : Sending User Email To %s\n",
		u.Name)
}

func (a Admin) Notify() {
	fmt.Printf("Admin: Sending Admin Email To %s. His address is \"%s\".\n",
		a.Name, // Verwenden des eingebetteten Feldes 'Name' vom User
		a.Email)
}

func main() {
    // Eine Variable vom Interfacetyp anlegen
    var notifier Notifier
    
	// Einen User und einen Admin erstellen
	user := User{
		Name: "john smith",
	}
	admin := Admin{
		User: user,
		Email: "john@email.com",
	}

	// Die implementierte Notify-Methode aufrufen
	// Die Methode wird über die Interfacevariable aufgerufen
	// Mittels dynamischer Bindung wird die Methode am richtigen Typ aufgerufen
	notifier = user
	notifier.Notify()
	notifier = admin
	notifier.Notify()
}

Nebenläufigkeit

Zur Unterstützung der nebenläufigen Programmierung in Go wird das Konzept der Kanäle (channels) genutzt, das eine relativ stark abstrahierte Möglichkeit der synchronen oder asynchronen Kommunikation zwischen Go-Routinen bietet. Ein Kanal ist dabei ein Speicherbereich, der durch Semaphore abgesichert ist und eine Warteschlange (buffered/asynchronous channel) oder lediglich eine Schnittstelle (unbuffered/synchronous channel) zur Verfügung stellt.[12] Über einen Kanal lassen sich dabei nur Daten eines festen Typs übertragen. Hierbei ist jedoch keinerlei Begrenzung hinsichtlich des Typs gegeben, auch Channels für Channels sind denkbar.[13]

Ein Kanal wird durch den Aufruf make(chan typ) (synchron) bzw. make(chan typ, größe) (asynchron, wenn größe > 0) erstellt. Anschließend können Go-Routinen in den Channel schreiben, von ihm lesen und ihn schließen.

Bei synchronen Kanälen blockiert ein Lesezugriff, bis eine andere Go-Routine in den Channel schreibt, bzw. der Schreibzugriff, bis eine andere Routine liest. Bei asynchronen Kanälen tritt ein solches Verhalten nur auf, wenn der zu lesende Channel leer bzw. der zu schreibende Channel voll ist. Es gibt in Go keine Beschränkung hinsichtlich der Anzahl an Go-Routinen, die einen Channel lesen und schreiben. Trotz der ausgefeilten Synchronisationsmechanismen kann bei der Benutzung von Channels ein Deadlock auftreten, welcher die Go-Laufzeitumgebung veranlasst, das Programm zu beenden. Eine Go-Routine kann über das select Konstrukt auf mehreren Channels gleichzeitig lauschen, bzw. versuchen, in mehrere Channels zu schreiben, wobei dasjenige case-Statement ausgeführt wird, welches zuerst nicht mehr blockiert, oder im Fall mehrerer Optionen eine pseudo-zufällige Wahl getroffen wird.

Daten werden mit kanal <- Wert in einen Kanal geschrieben und mit variable = <- kanal gelesen, wobei beim Lesen die Variablenzuweisung wegfallen kann. Das Lauschen auf einem Channel kann auch mit dem for-Konstrukt automatisiert werden, wobei die Schleife verlassen wird, sobald der Channel geschlossen ist.

Beispiel:

package main

import "fmt"

func zehnMal(kanal chan string) {
    // Argument empfangen
    sag := <- kanal

    // Zehnmal zurückschreiben
    for i := 0; i < 10; i++ {
        kanal <- sag
    }

    // Kanal schließen
    close(kanal)
}

func main() {
    // synchronen Kanal öffnen
    kanal := make(chan string) // oder make(chan string, 0)

    // Starten der parallelen Go-Routine „zehnMal()“
    go zehnMal(kanal)

    // Senden eines Strings
    kanal <- "Hallo"

    // Empfangen der Strings, bis der Channel geschlossen wird
    for s := range kanal {
        fmt.Println(s)
    }

    fmt.Println("Fertig!")
}

Im Beispiel ruft main() die Go-Routine zehnMal() auf, die einen empfangenen String zehnmal über den gleichen Kanal zurückgibt und ihn danach schließt. Durch den synchronen Kanal warten die beiden Go-Routinen aufeinander, sodass main() erst in die for-Schleife eintritt, wenn zehnMal() den String empfangen hat. Wäre der Kanal nicht synchron, könnte ein Deadlock auftreten, wenn main() die geschriebene Variable sofort wieder liest (und aus dem Puffer entfernt) und zehnMal() vergeblich auf sein Argument wartet. Wichtig ist auch, dass zehnMal() nach dem Schreiben der Strings den Kanal schließt, da main() sonst die Schleife nicht verlassen kann.

Implementierungen

Es gibt mindestens zwei Compiler für Go, die auf Linux, macOS, Windows und FreeBSD betrieben werden können und die Go-1-Spezifikation vollständig implementieren:

Gc
ist der offizielle Go-Compiler und wurde initial von Ken Thompson in C geschrieben, basierte auf der Plan 9 Toolchain und nutzte Yacc/Bison zum Parsen. Mit Version 1.5 wurde dieser Compiler von C nach Go übersetzt und ist damit self-hosting. Ursprünglich bestand der Compiler aus mehreren ausführbaren Kommandos, die unterschiedliche Namen je nach Ziel-Architektur hatten: 8 g für x86, 6 g für x86_64, 5 g für ARM. Mit Version 1.5 wurden sie zu einem einzelnen ausführbaren Kommando zusammengefasst (go tool compile), und die Ziel-Architektur kann über die Umgebungsvariable GOARCH gewählt werden.
Gccgo
von Ian Taylor ist ein Go-Frontend für die GNU Compiler Collection (GCC). Das in C++ geschriebene Frontend nutzt zum Parsen einen rekursiven Abstieg. Die folgenden Backend-Schritte sind die der Standard-GCC-Verarbeitung[14]. Durch dieses Vorgehen wird zwar die Kompilierzeit im Vergleich zum Gc-Compiler erhöht, jedoch ist der produzierte Code effizienter. Die GNU Compiler Collection (GCC) unterstützt Go 1 mit Version 4.7.1 vollständig,[15] der GNU Debugger (gdb) unterstützt Go ab Version 7.5.[16]

Beide Compiler implementieren eine parallele Mark-and-Sweep-Speicherbereinigung.

Der offizielle Compiler wird von dem Kommandozeilen-Werkzeug go begleitet, das als Fassade für verschiedene Werkzeuge dient, wie z. B. dem Installieren von Paketen aus Quelltext-Repositories im Internet wie etwa GitHub oder Google Code (go get), dem automatischen Formatieren von Quelltext (go fmt), dem Ausführen von Tests (go test), dem Erzeugen von Dokumentation aus Quelltext-Kommentaren (go doc) oder dem Kompilieren des Projektes (go build), so dass keinerlei Makefiles nötig sind, wenn eine empfohlene Verzeichnisstruktur eingehalten wird.

Geschichte

Die Entwurfsphase begann am 21. September 2007, anfangs als 20-Prozent-Projekt auf Initiative von Robert Griesemer, Rob Pike und Ken Thompson. Bald darauf stießen weitere Entwickler dazu, und Go wurde zum Vollzeit-Projekt.[17] Am 30. Oktober 2009 wurde Go von Rob Pike in einem Google TechTalk präsentiert und die Veröffentlichung als freie Software angekündigt, die dann wie angekündigt am 10. November erfolgte.[18][19] Seitdem sind zahlreiche Beiträge von Entwicklern aus der Go-Community außerhalb Googles hinzugekommen. Am 28. März 2012 wurde Version 1 freigegeben.[20] Seitdem gelten Sprachspezifikation und Standardbibliothek als stabil und sollen innerhalb der 1.x-Serie auf Quelltext-Ebene abwärtskompatibel bleiben.[21] Am 14. Mai 2013 wurde Go 1.1 freigegeben, das vor allem Performance-Verbesserungen an der Implementierung enthält.[22] Jeweils sechs Monate später erschienen die Versionen Go 1.2 bis Go 1.10.

Vom 24. bis zum 26. April 2014 fand die erste Konferenz zu Go, die GopherCon,[23] in Denver statt, welche seitdem jährlich stattfindet.

Maskottchen

Das Gopher-Maskottchen von Go

Das Go-Maskottchen ist eine Taschenratte (englisch Gopher). Es wurde von Renée French entworfen, die auch Glenda, das Plan-9-Häschen, entworfen hat. Das Logo und das Maskottchen stehen unter der Creative Commons Attribution 3.0-Lizenz.

Literatur

  • Alan A. A. Donovan, Brian W. Kernighan: The Go Programming Language. Pearson Education, 2015, ISBN 978-0-13-419044-0.
  • Frank Müller: Systemprogrammierung in Google Go: Grundlagen, Skalierbarkeit, Performanz, Sicherheit. dpunkt.verlag, Heidelberg 2011, ISBN 978-3-89864-712-0.
  • Rainer Feike: Programmierung in Google Go: Neuigkeiten von Google in der Systemprogrammierung. Addison-Wesley, München/Boston 2010, ISBN 978-3-8273-3009-3.
  • Caleb Doxsey: An Introduction to Programming in Go. 2012, ISBN 978-1-4783-5582-3 (englisch, golang-book.com).
  • Andreas Schröpfer: Go – Das Praxisbuch. dpunkt.verlag, 2020, ISBN 978-3-86490-713-5.
  • Kristian Köhler: Microservices mit Go. Rheinwerk Verlag, 2020, ISBN 978-3-8362-7559-0.

Weblinks

Einzelnachweise