Go (auch Golang) ist eine kompilierbare Programmiersprache, die Nebenläufigkeit und automatische Speicherbereinigung unterstützt. Entwickelt wurde Go von Mitarbeitern des Unternehmens Google Inc.Google-go-language Die Entwürfe stammen von Robert Griesemer, Rob Pike und Ken Thompson.

Überblick Go wurde von Google entwickelt, um Problemen, die C, C++, Java und Co. mit sich brachten, zu begegnen. Insbesondere Probleme im Hinblick auf skalierbare Netzwerkdienste, Cluster- und Cloud Computing haben das Team dazu geführt, Go zu entwickeln.Why Learn Go? – Kurzinterview mit Rob Pike (Video, englisch) Vor allem hat Go viel weniger Schlüsselwörter als die Sprachen aus der C-Familie. Zudem sollte Go eins lösen: die Compiler-Ineffizienz in der C-Familie. 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.Go at Google: Language Design in the Service of Software Engineering Go besitzt einen eigenen Garbage Collector, erlaubt die Verwendung von Zeigern, verzichtet jedoch auf Zeigerarithmetik.go.dev – FAQ-Eintrag zu Zeigerarithmetik Beim Sprachdesign wurde Wert auf schnelle Compilierbarkeit gelegt.

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.Origins of Go’s Concurrency Model

Merkmale und Sprachmittel Go bietet Closures und Reflexion 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.

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.go.dev – FAQ-Eintrag zu Unicode-Bezeichnern

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.go.dev Blogeintrag zu Syntax für Deklarationen 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 Einsprungpunkt 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, er 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") }

Das obige Hallo-Welt-Programm 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*4 //Die Leibniz-Reihe berechnet Pi/4 }

// berechnet das k-Element der Leibniz-Reihe func calcsubterm(ch chan<- float64, k float64) { ch <- 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.Berechnung von Pi auf dem Go-Playground (Beispiel zur nichtsequentiellen Berechnung von Pi) 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. 8-Bit-„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 so leichter erkannt werden können.

var i int = 100 var f float64 = float64(i) var u uint = uint(f)

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:

i := 100 f := float64(i) u := uint(f)

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 „n“ und dem Wert 100 initialisiert. Anschließend wird die Speicheradresse, also die Adresse im Speicher des Computers ausgegeben. Die Adresse der Variable „n“ wird also mit &n abgerufen. Die Syntax der Zeiger erinnert stark an die Sprache C.

var n int = 100 fmt.Println("Die Adresse der Variable:", &n) // Beispiel-Ausgabe: 0xc000018030

Im nächsten Beispiel wird wieder eine Variable vom Typ Integer mit dem Namen „n“ 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 „n“ als Wert der Zeiger-Variable „p“ deklariert. Daraufhin wird jene Adresse des Speichers ausgegeben, die von der Variable „n“ belegt wurde. Zuletzt wird jener Wert ausgegeben, der sich in der Speicheradresse befindet. Der Wert einer Speicherstelle, der in der Zeiger-Variable „p“ deklariert ist, kann mit *p ermittelt werden.

var n int = 100 var p *int = &n

// Gibt eine Speicheradresse aus, z. B. 0xc000018030 fmt.Println("Adresse gespeichert in der Zeiger-Variable p:", p)

// Gibt den Wert an, der in der Speicheradresse steht: 100 fmt.Println("Wert der Speicheradresse gespeichert in p:", *p)

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 p != nil { // Wenn zutreffend: Zeiger-Variable speichert einen Zeiger auf einen Speicherbereich } if p == 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.

package main

import "fmt"

func main() { a := 10 b := 50

// Anzeige vor dem Tausch fmt.Println("Wert der Variable a:", a) fmt.Println("Wert der Variable b:", b)

// Aufruf der Funktion tauschen tauschen(&a, &b)

// Anzeige des Tausches fmt.Println("Wert der Variable a:", a) fmt.Println("Wert der Variable b:", b) }

// Funktion tauschen func tauschen(a, b *int) { tmp := *a *a = *b *b = tmp }

Man kann also grob zusammenfassen, dass & die Speicheradresse einer Variable ermittelt, während * den gespeicherten Wert einer Speicheradresse ermittelt.A Tour Of Go – Pointers

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, sodass 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', indem sie jeweils eine // entsprechende 'Notify()'-Methode haben. 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. The email address is %q.\n", a.Name, // Verwenden des eingebetteten Feldes 'Name' vom User a.Email) }

func main() { // Einen User und einen Admin erstellen user := User{ Name: "john smith", } admin := Admin{ User: user, Email: "[email protected]",

// Eine Variable vom Interfacetyp anlegen var n Notifier

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

Nebenläufigkeit