Skip to content
Harald Weidner edited this page Apr 10, 2022 · 7 revisions

Die Syntax von Go in Beispielen

In diesem Artikel wird die Syntax von Go anhand von Beispielen erläutert.

Hello World

Das beliebte "Hello World" Beispiel sieht in Go so aus:

// hello.go

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

Das Programm kann direkt ausgeführt werden:

$ go run hello.go
Hello, World!

Dabei wird das Programm compiliert, in einem temporären Verzeichnis abgelegt und dort ausgeführt. Man kann es auch explizit bauen:

$ go build hello.go
$ ./hello
Hello, World!

Anhand dieses Programms sieht man einige Eigenschaften von Go:

  • Kommentare werden, wie in C++, entweder zeilenweise mit // eingeleitet oder zeilenübergreifend mit /* begonnen und */ beendet.
  • Jede Quelltextdatei gehört zu einem Package. Ein Package kann aus einer oder mehrerer Dateien bestehen. Die erste Zeile einer Quelltextdatei gibt an, zu welchem Package die Datei gehört. Ein Programm (command) muss mindestens das Package main definieren, und darin muss es eine Funktion main geben. Eine Bibliothek (library) hat kein Package main.
  • In einer Quelltextdatei können andere Packages importiert werden, um auf deren Funktionen zuzugreifen. Hier wird das Package fmt (formatted I/O) importiert; es beinhaltet Funktion für die formatierte Ein- und Ausgabe.
  • Eine Funktion wird mit dem Keywort func eingeleitet. Die Hauptfunktion main wird zu Beginn eines Programmes aufgerufen. Der Inhalt der Funktion wird, wie in C/C++ oder Java, in geschweifte Klammern { } eingeschlossen.
  • Beim Zugriff auf Funktionen eines Package wird der Package-Name (hier: fmt) dem Funktionsnamen (hier: Println) vorangestellt. Die Funktion wird für die Ausgabe beliebiger Daten auf der Standardausgabe verwendet; sie setzt automatisch einen Zeilenumbruch hinter die Ausgabe.
  • Es wird kein Semikolon am Ende einer Anweisung benötigt. Mehrere Anweisungen können aber in einer Zeile geschrieben werden; sie werden dann durch das Semikolon getrennt.

Packages

Ein Go-Programm kann in mehrere Packages unterteilt werden. Bei einem ausführbaren Programm (command) muss mindestens das Package main vorhanden sein. Eine Bibliothek besteht i.d.R. aus einem package, kann aber auch mehrere (interne) Packages beinhalten.

Innerhalb eines Package werden Variablen, Konstanten, Funktionen, Typen, Methoden und Interfaces definiert. Nur wenn deren Name mit einem Großbuchstaben beginnt, sind sie "exportiert", d.h. von außen (in anderen Packages, die dieses Package importieren) sichtbar. Ansonsten können die Bezeichner nur innerhalb des Package verwendet werden.

Andere Package werden mit import "Package" eingebunden. Es dürfen nur Packages eingebunden werden, die auch tatsächlich benutzt werden. Ein zyklisches Einbinden ist nicht erlaubt; wenn also Package A ein Package B importiert, darf B weder direkt noch indirekt A importieren.

Ein Package kann aus einer oder mehrerer Dateien bestehen. Üblicherweise befinden sich diese im selben Verzeichnis, und der Name des Verzeichnisses entspricht dem Package-Namen.

Packages werden mit vollem Pfadnamen importiert, z.B. import "net/http". Bei der Benutzung der Funktion wird nur der Package-Name angegeben, z.B. http.ListenAndServe(). Bei Mehrdeutigkeiten ist es möglich, den Namespace lokal umzuschreiben, z.B. durch import rnd crypto/rand. Die Funktionen der Bibliothek sind nun mit rnd.Name() erreichbar.

Seit Go 1.11 gibt es das Konzept der Module. Ein Modul besteht aus einem oder mehreren Packages. Zudem enthält die Modulspezifikation eine Angabe der vorausgesetzten Go-Version sowie Versionsbezeichnungen der abhängigen Module. Zu Modulen wird es hier demnächst einen eigenen Artikel geben.

Skalare Datentypen

Variablen

// Beispiele für Deklarationen

var a int
var b int8 = 1
var c = int64(2)

Der Deklaration einer Variablen wird das Schlüsselwort var vorangestellt. Der Typ steht hinter der Variablen. Optional kann eine Initialisierung (Zuweisung eines Wertes) erfolgen. Der Typ kann weggelassen werden, wenn er aus der Initialisierung bereits hervor geht.

Bei einer Deklaration ohne Initialisierung wird die Variable automatisch mit einem Defaultwert initialisiert. Dies ist, je nach Typ, die Null, der leere String, der logische Wert false oder der Nullpointer nil. Es ist erlaubt, eine Variable zu nutzen, ohne ihr vorher (explizit) einen Wert zugewiesen zu haben.

// nur in Funktionen erlaubt:

d := 3   // entspricht: var d int = 3

Innerhalb von Funktionen gibt es eine weitere Variante der Variablendeklaration, bei der der Typ und das Schlüsselwort var weggelassen werden können. Statt des Zeichens = wird := benutzt. Der Typ der Variable ergibt sich aus dem Typ des Ausdrucks rechts des := Operators.

Eingebaute (skalare) Datentypen in Go sind:

  • bool für Wahrheitswerte (false oder true)
  • int ist ein maschinenabhängiger, vorzeichenbehafteter Integer-Typ. Auf 32-Bit Systemen ist er 32, auf 64-Bit Systemen 64 Bit breit.
  • int8 int16 int32 int64 sind maschinenunabhängige, vorzeichenbehaftete Integer-Typen der angegebenen Breite in Bit.
  • uint uint8 uint16 uint32 uint64 sind analog dazu vorzeichenlose Integer-Typen.
  • float32 float64 sind 32 bzw. 64 Bit breite Gleitkomma-Typen.
  • complex64 complex128 sind Typen für komplexe Zahlen. Ein complex64 besteht aus zwei float32 Komponenten, ein complex128 aus zwei float64.
  • byte ist ein Alias für uint8 und wird für Zeichen in US-ASCII benutzt.
  • rune ist ein Alias für int32 und wird für Unicode Codepoints in UTF-8 Kodierung genutzt.
  • string ist ein Datentyp für eine (konstanten) UTF-8 Zeichenkette.
  • uintptr ist ein Typ für Zeiger. Die Breite ist plattformabhängig, es gilt nur die allgemeine Aussage, dass alle Zeiger darin speicherbar sein müssen.
  • error ist ein eingebauter Typ für die Fehlerbehandlung.

Konstanten

// Konstanten

const Pi float64 = 3.14159265358979323846
const zero = 0.0
const x, y, z = 5, 17.3, "foo"
const ti int = 42                // typisierte Konstante

Konstanten werden syntaktisch analog zu Variablen deklariert. Auf der rechten Seite darf nur ein konstanter Ausdruck stehen. Konstanten sind können typisiert (typed) oder typenlos (untyped) sein. Typenlose Konstanten nehmen erst im Kontext ihrer Verwendung einen konkreten Typ an.

Eingebaute Konstanten in Go sind

  • nil - der Null-Zeiger
  • false und true, die Wahrheitswerte für bool Variablen
  • iota - ein Zähler für Konstanten, s.u.

Das folgende Beispiel vereint mehrere syntaktische Besonderheiten. Es definiert die Konstanten Sonntag, Montag, …, Samstag zu den Zahlenwerten 0 bis 6 sowie anzahlTage auf 7.

const (
    Sonntag = iota
    Montag
    Dienstag
    Mittwoch
    Donnerstag
    Freitag
    Samstag
    anzahlTage   // nicht extern sichtbar
)
  • Anstatt das Wort const vor jede Zeile zu schreiben, können mehrere Konstanten auch innerhalb einer const ( ... ) Klammer definiert werden. Diese Klammerung ist auch mit Variablen oder Imports möglich.
  • Die eingebaute Konstante iota ist ein Zähler, der innerhalb einer const ( ... ) Klammerung gilt. Er beginnt mit 0 und erhöht sich in jeder Zeile um 1.
  • Wenn hinter der Konstante (z.B. Montag) keine Zuweisung steht, wiederholt sich die Zuweisung der letzten Zeile. Somit werden in diesem Beispiel die Wochentage aufsteigend durchnummeriert.
  • Nur Konstantennamen, die mit einem Großbuchstaben beginnen, sind außerhalb des Packages sichtbar. Andere können nur innerhalb des selben Packages benutzt werden. Das gleiche gilt für Variablen, Funktionen und selbstdefinierte Typen.

Zeiger

var a int
var b *int // Zeiger auf int

a = 15
b = &a  // b ist ein Zeiger auf a
*b = 20 // a ist nun 20

Ein Zeiger (pointer) ist eine Variable, die eine Referenz auf eine andere Variable enthält. Im obigen Beispiel ist b ein Zeiger auf die Variable a. Zur Deklaration des Zeigertyps und zur Dereferenzierung (Auswertung des Objekts, auf das ein Zeiger zeigt), dienst der * Operator. Die Adresse einer Variablen erhält man mit dem & Operator.

Go besitzt zwar Zeiger, aber keine Zeigerarithmetik; es ist also nicht möglich, durch Rechenoperationen einen Zeiger auf andere Objekte zu erhalten.

func foo() {
    x := new(int) // Zeiger auf anonymes int
    *x = 23
} // int wird durch Garbabe Collector gelöscht

Mit der eingebauten Funktion new() wird ein neues, anonymes Objekt des angegebenen Typs erstellt und ein Zeiger darauf zurück gegeben. Es gibt keinen Variablennamen zu diesem Objekt, es ist nur über den Zeiger erreichbar. Anders als z.B. in C++ können Objekte nicht explizit gelöscht werden. Ein Go Programm erkennt automatisch, wenn es auf ein Objekt keine Referenz mehr gibt; das Objekt wird dann durch die Garbage Collection gelöscht.

Ein Zeiger in Go kann somit nur entweder nil sein oder auf ein gültiges Objekt zeigen. Die Dereferenzierung eines nil Zeigers führt zu einer Laufzeit-Fehlermeldung.

Funktionen

func mult(a, b int64) int64 {
    return a * b
}

func add(a, b int64) (c int64) {
    c = a + b
    return
}

func main() {
    x := mult(3, 8)
    y := add(x, 10)
    fmt.Println(x, y)
}

Funktionen werden mit dem Schlüsselwort func eingeleitet. Danach folgt der Funktionsname, in Klammern eine Liste der Parameter sowie den Typ des Rückgabewertes. Wenn der Rückgabewert auch namentlich benannt wird oder es mehrere Rückgabewerte gibt, müssen diese ebenfalls in Klammern geschrieben werden.

Funktionen mit mehreren Rückgabewerten

func foo() (int, error) {
    x := ...
    return x, nil
}

func main() {
    d, err := foo()
    if err != nil {
        panic(err)
}
    ...
}

Das angegebene Beispiel zeigt die gängige Methode der Fehlerbehandlung in Go. Wenn die Funktion fehlerfrei ausgeführt wird, werden das Ergebnis und nil als Fehler zurückgegeben; ansonsten hat das Ergebnis einen neutralen Wert und die Fehlervariable ist mit einem Wert besetzt, der zum Typ error (ein Interface, dazu mehr später) passt.

Anonyme Funktionen

func main() {
    k := func (i, j int) int {
             return i * j
         } (14, 9)
    ...
}

Funktionen können anonym, d.h. ohne Funktionsnamen, verwendet werden. Im Beispiel wird eine Funktion definiert und sogleich mit Parametern aufgerufen. Abgesehen vom fehlenden Namen erfolgt die Definition genauso wie oben beschrieben.

Variadische Funktionen

func summe(s ...float64) float64 {
    sum := 0.0
    for _, i := range s {
        sum += i
    }
    return sum
}

func main() {
    x := summe(1.5, 3, -4.0)
    fmt.Println(x)
}

// auch erlaubt:
a := []float64{1.5, 3, -4.0}
y := summe(a...)

Funktionen können eine variable Anzahl von Parametern haben, wobei diese vom gleichen Typ sein müssen. Innerhalb der Funktion sind die Werte in einem Slice verfügbar (dazu später mehr). Umgekehrt kann ein Slice genutzt werden, um Parameter an eine Funktion mit variadischer Parameterliste zu übergeben.

Ausführung am Ende einer Funktion

func foo() {
    lock(l)
    defer unlock(l)
    ...
    // unlock geschieht hier
}

Mit dem Schlüsselwort defer kann die Ausführung einer Anweisung an das Ende einer Funktion verschoben werden. Somit lassen sich zusammengehörige Operationen (z.B. lock/unlock oder open/close) optisch zusammen schreiben, obwohl sie im Kontrollfluß weit auseinander liegen. defer ist insbesondere praktisch, wenn mehrere Dinge geöffnet werden (z.B. Netzwerkverbindung, File, Datenbankverbindung) und bei jedem davon Fehler auftreten können.

Kontrollstrukturen

for-Schleife

// for-Schleife

summe := 0
for i := 1; i <= 10; i++ {
     summe += i
}

// while Schleife

for ; i <= 10; { ... }
// oder
for i <= 10 { ... }

// Endlosschleife

for { ... }

Die for-Schleife setzt sich, wie bei vielen anderen Sprachen auch, aus einer Initialisierung, einer Endbedingung und einem Increment zusammen; die drei Teile werden durch Semikolon getrennt. Indem man den ersten und dritten Teil weg lässt, wird daraus eine while-Schleife. In diesem Fall können auch die beiden Semikolon entfallen. Eine for-Schleife ohne Endbedingung ist eine Endlosschleife.

Innerhalb der Schleife springt continue zum nächsten Schleifendurchlauf, break verlässt die Schleife. Bei verschachtelten Schleifen können Labels gesetzt werden, so dass mit break auch die äußere Schleife verlassen werden kann.

if-Abfrage

// if

if a < 0 { a = -a } 

// if-else

if x := foo(); x < A {
    return A
} else if x > B {
    return B
} else {
    return x
}

Es gibt keine eigenständige elsif Anweisung. Statt dessen kann else if geschachtelt werden, geschweifte Klammern hinter else können entfallen.

switch/case Abfrage

// switch

switch m {
    default: foo()
    case 0, 2, 4, 6: bar()
    case 1, 3, 5, 7: biz()
}

// Switch ohne Variable
switch {
    case a < b: foo()
    case a > c: bar()
    case b == d: biz()
}

Bei der switch-Anweisung werden die Fälle (case) von oben nach unten abgearbeitet. Die Position der default-Bedingung ist egal; sie wird in jedem Fall nur angesprungen, wenn sonst keiner der Fälle zutrifft.

Nach der Ausführung der Anweisungen hinter einem Fall wird die Abarbeitung beendet. Es ist kein separater Ausstieg (wie z.B. break in C) nötig. Mit fallthrough kann direkt in den nächsten Fall gesprungen werden, auch wenn die Bedingung dort nicht zutrifft.

Eine Besonderheit ist die switch-Anweisung ohne Variable. Hier können hinter den einzelnen case Statements vollständige Vergleiche stehen, die nicht notwendigerweise disjunkt sein müssen. Eigentlich ist das also eine kürzere Schreibweise für if Abfragen.

Zusammengesetzte Datentypen

Verbundtypen

Eigene Datentypen werden mit type definiert. Zur Definition eines Verbundtyps dient struct.

// Verbundtyp

type Person struct {
    ID int 
    Vorname string 
    Nachname string 
} 

// Deklarationen
a := Person{1, "Harald", "Weidner"} 

b := Person{                  // ID bleibt 0
    Vorname: "Erika",
    Nachname: "Mustermann",
}

fmt.Println(a, b)

// ergibt:
// {1 Harald Weidner} {0 Erika Mustermann}

Eine Variable von einem Verbundtyp kann direkt initialisiert werden. Dazu wird die Schreibweise Person{...} genutzt. Innerhalb der geschweiften Klammern werden entweder alle Felder in der Reihenfolge der Typdefinition belegt, oder es wird jeweils der Feldname und -inhalt angegeben. Im letzteren Fall können Felder weggelassen werden; sie werden mit dem Defaultwert des jeweiligen Typs gefüllt (Null, Leerstring, nil oder false).

Methoden

Für Verbundtypen (oder andere selbst definierten Typen) können Methoden definiert werden.

// Methoden

func (p Person) FullName() string {
    return p.Vorname + " " + p.Nachname
}

fmt.Println(a.FullName())
// ergibt: Harald Weidner

Eine Methode ist eine Funktion, die für einen bestimmten Typ definiert wird. Der Typ steht links des Funktionsnamens in Klammern. Aufgerufen wird die Methode mit der Syntax Variable.Methode().

In Go gibt es keine spezielle Notation einer Klasse. Methoden können mit (fast) allen selbst definierten Typen verwendet werden. Es gibt nur zwei Ausnahmen:

  • Interface-Typen, also Typen der Art type Foo interface { ... }
  • Pointer-Typen, z.B. type Pint *int

Interface-Typen können keine Methoden erhalten, da Interfaces zur Trennung von Spezifikation und Implementierung verwendet werden. Die Interfaces enhalten eine Spezifikation der Methoden, die vorhanden sein müssen; die konkreten Typen implementieren diese Methoden.

Pointer-Typen dürfen keine Methoden haben, da ansonsten die Regeln, welche Methoden zu einem Typ gehören, zu kompliziert würden. Pointer-Typen sind generell kein guter Stil und sollten vermieden werden.

Spezielle Methoden

func (p Person) String() string {
    return fmt.Sprintf("[%d] %v %v", p.ID, p.Vorname, p.Nachname)
}

fmt.Println(b)
// ergibt:
// [0] Erika Mustermann

Manche Methoden haben eine spezielle Bedeutung für Funktionen der Go Standardbibliothek. Ein Beispiel hierfür ist String(). Wenn diese Methode existiert und einen Rückgabewert vom Typ string hat, dann wird sie von den Ausgabefunktionen wie fmt.Println() benutzt, um den Inhalt der Variablen auszugeben.

Interfaces

// Interface

type FullNamer interface {
    FullName() string
}

var x, y FullNamer

x = Person{23, "Hagbard", "Celine"}   // erlaubt
y = int(3)                            // Fehler, int erfüllt interface nicht

Ein Interface besteht aus einer Liste von Methoden, die ein Typ besitzen muss, um das Interface zu erfüllen (“implementieren”). Ob ein Typ ein Interface erfüllt, wird nicht explizit angegeben, sondern ergibt sich implizit aus der Definition des Typs und seiner Methoden.

Das Interface wird selber als benutzerdefinierter Typ definiert. Es kann also Variablen vom Typ des Interfaces geben. Auf diese Weise wird Polymorphie in Go realisiert.

Ob ein Typ ein bestimmtes Interface erfüllt, kann zur Laufzeit geprüft werden. Von dieser Möglichkeit macht die Go Standardbibliothek häufig Gebrauch.

Es ist eine Konvention, dass ein Interface mit nur einer Methode den Namen der Methode mit angehängtem “-er” bekommt. Ein Interface, dessen einzige Funktion String() heißt, erhält also den Namen Stringer.

Das leere Interface interface{} wird von allen Typen erfüllt. Einer Variable vom Typ interface{} können also Variablen jedes beliebigen anderen Typs zugewiesen werden. So lassen sich Funktionen definieren, die mit beliebigen Argumenten umgehen können. Bekanntestes Beispiel dafür sie die Ausgabefunktionen wie fmt.Println(), der man Variablen und Konstanten aller Typen übergeben kann. Mittels der Reflection API prüft die Funktion, welchen konkreten Typ ein Argument hat, und verhält sich entsprechend. Das geschieht bei Bedarf auch rekursiv, z.B. bei Verbundtypen oder Arrays.

Array

// Arrays

var a [5]int64                  // Array mit 5 Feldern
var b = [3]int{1, 2, 3}         // entspricht [1, 2, 3]
var c = [...]int{1, 7, 4, 8, 3} // automatische Längenerkennung

Arrays sind statisch, d.h. in der Länge nicht änderbar. Die Inhalte können natürlich geändert werden.

Slice

// Slices

var s []int                   // uninitialisierter Slice
var t = make([]int32, 5)      // Slice der Länge 5
var r = make([]int32, 5, 10)  // Slice der Länge 5 und Kapazität 10

s = c[1:4]                    // s ist [7, 4, 8]

r = append(r, 9)              // Anhängen eines Elementes, len(r) ist jetzt 6
t = append(t, 1)              // Kapazitätserweiterung nötig, t wird umkopiert

Ein Slice ist eine Referenz auf ein (anonymes) Array oder einen Ausschnitt eines Arrays. Ein Slice besitzt eine Länge und eine Kapazität, die gleich oder größer der Länge ist. Die Länge kann verkleinert oder bis zum Erreichen der Kapazität vergrößert werden.

Die eingebaute Funktion append() hängt Elemente an einen Slice an. Wenn die Kapazitätsgrenze erreicht ist, erzeugt sie dabei einen neuen, doppelt so großen Slice und kopiert die Elemente um.

// copy

s1 := []int{1, 2, 3, 4}
s2 := make([]int, 3)
copy(s2, s1)
fmt.Println(s2)  // ergibt: [1 2 3]

Die eingebaute Funktion copy(dest, src) kopiert einen Slice in einen anderen. Es werden dabei höchstens so viele Elemente kopiert, dass die Länge des Ziel-Slice nicht überschritten wird. Die Länge und Kapaztät des Slice, in den kopiert wird, bleibt unverändert. Die Daten werden physisch kopiert, eine anschließende Änderung des ursprünglichen Slice s1 ändert nicht die Inhalte von s2 und umgekehrt.

Bei der Zuweisung eines Slice zu einem anderen (s2 = s1) findet dagegen keine Kopie der Inhalte statt. Beide Slices referenzieren anschließend die selben Daten; nach einer Änderung des ursprünglichen Slice wie s1[0] = 5 ändert sich auch s2[0] zu 5.

Map

// Map

var Size map[string]float32

Size["Alice"] = 1.67
Size["Bob"] = 1.82 

age := map[Person]uint {
    Person{1, "Harald", "Weidner"}: 43,
    Person{2, "Erika", "Mustermann"}: 29,
}

for k, v := range age {
    fmt.Println(k, „=>“, v)
}

Eine Map ist ein assoziatives Array, vergleichbar mit dem Hash in Perl oder einem Dictionary in Python. Bei der Definition einer Map werden Schlüssel und Werte-Typ angegeben. Für den Schlüsseltyp muss es dabei eine Vergleichsfunktion (= bzw. !=) geben, damit entschieden werden kann, ob ein Schlüssel bereits vorhanden ist oder nicht. Ansonsten können beide Typen beliebig gewählt werden.

Das Keyword range kann genutzt werden, um über alle Elemente einer Map zu iterieren. Es kann auch für Arrays, Slices oder Strings verwendet werden.

Nebenläufige Programmierung

Goroutine

// Goroutine

import (
    "fmt"
    "time"
)

func foo() {
    fmt.Println("foo")
}

func bar() {
    fmt.Println("bar")
}

func main() {
    go foo()
    go bar()
    time.Sleep(time.Second)
}

Goroutinen sind nebenläufige Funktionen. Eine Goroutine wird wie eine Funktion mit dem vorangestellten Schlüsselwort go aufgerufen. Sie terminiert am Ende der aufgerufenen Funktion, spätestens aber mit dem Ende des Hauptprogrammes. Um den Effekt zu zeigen wartet in obigem Beispiel das Hauptprogramm eine Sekunde.

Das Scheduling der Goroutinen wird von der Go Runtime übernommen. In der Regel werden viele Goroutinen auf wenige Betriebssystem-Threads verteilt. Dadurch sind Goroutinen sehr effizient. In der aktuellen Go Runtime benötigt eine Goroutine lediglich ein initiales Stacksegment von 2kB. Es ist problemlos möglich, mehrere Hundertausend Goroutinen nebenläufig zu betreiben.

import "runtime"

func main() {
    runtime.GOMAXPROCS(16)
    ...
}

Um die Kapazitäten von Mehrkernprozessoren auszunutzen, muss die Runtime angewiesen werden, Goroutinen auf mehrere Threads zu verteilen. Dazu kann die Funktion runtime.GOMAXPROCS verwendet werden. Alternativ ist es möglich, die Environment-Variable GOMAXPROCS zu setzen. Im Beispiel wird die Runtime angewiesen, fix 16 Threads zu verwenden. Der Default ist, so viele Threads zu verwenden, wie das System (virtuelle) CPU-Cores hat.

Channel

// Channel

func fib(c chan int) {
    f0, f1 : = 0, 1
    for {
        c <- f0
        f0, f1 = f1, f0+f1
    }
}

func main() {
    c := make(chan int)
    go fib(c)
    for i := 0; i < 25; i++ {
        fmt.Println(<- c)
    }
}

Ein Channel ist ein typisierter Transportkanal. Er kann bidirektional verwendet werden, um Daten des angegebenen Typs zu transportieren. Der Channel kann von allen Goroutinen und Funktionen verwendet werden, die ihn kennen. Typischerweise wird ein Channel im Hauptprogramm erzeugt und dann an die Goroutine als Argument übergeben.

// gepufferter Channel

c := make(chan int, 5)

Ein Channel kann gepuffert oder ungepuffert sein. Ein ungepufferter Channel blockiert den Schreiber, bis von ihm gelesen wird, oder den Leser, bis der Schreiber bereit zum Schreiben ist. Ein gepufferter Channel kann eine angegebene Anzahl von Variablen zwischenspeichern und somit als Pipeline genutzt werden.

// Prüfung auf geschlossenen Channel

val, ok := <- c
// ok == false wenn c geschlossen oder leer

Ein Channel kann mit close() geschlossen werden. Beim Lesen kann geprüft werden, ob der Channel bereits geschlossen wurde.

// Multiplexing

var c1, c2 chan int
var i1, i2 int

select {
    case i1 = <- c1: { ... }
    case i2 = <- c2: { ... }
    default: { ... } // macht die Auswahl nichtblockierend
}

Eine Goroutine kann mit mehreren Channels arbeiten. Mittels einer select Anweisung bedient sie immer den Channel, der zum Lesen/Schreiben bereit ist. Wenn mehrere Channel bereit sind, ist die Reihenfolge undefiniert. Wenn keiner der Channel bereit ist, wird, falls vorhanden, die default Anweisung ausgeführt; ansonsten blockiert die Anweisung, bis einer der Channels bereit wird.

// Goroutine kontrolliert beenden

package main

import (
    "fmt"
    "time"
)

func fib(c chan int) {
    var f0, f1 int = 0, 1
    for {
        select {
            case c <- f0: {
                f0, f1 = f1, f0 + f1
            }
            case <- c: {
                fmt.Println("fib() has ended")
                return
            }
        }
    }
}

func main() {
    c := make(chan int)
    go fib(c)
    for i:=0; i<20; i++ {
        fmt.Println(<- c)
    }
    c <- 0
    time.Sleep(time.Second)
}

In diesem Beispiel kann das Hauptprogramm die Goroutine kontrolliert beenden. Dazu wird ausgenutzt, dass ein Channel bidirektional verwendet werden kann. Die Goroutine schreibt Fibonacci-Zahlen in den Channel, falls er dazu bereit ist, oder terminiert, wenn der Channel zum Lesen bereit ist. Das Hauptprogramm liest einige Werte und schreibt schließlich eine Zahl hinein, wodurch die Terminierung ausgelöst wird.

Reflection

Reflection bezeichnet die Möglichkeit einer Programmiersprache, zur Laufzeit den Typ einer Variable zu ermitteln oder Eigenschaften des Typsystems zu prüfen.

Go bietet leistungsfähige Möglichkeiten der Reflection. Eine davon ist der Type Switch:

// Type switch
func foo(t interface{}) {
    switch t := t.(type) {
        default:
            fmt.Printf("unexpected type %T\n", t)
        case bool:
            fmt.Printf("boolean %t\n", t)
        case int:
            fmt.Printf("integer %d\n", t)
        case *bool:
            fmt.Printf("pointer to boolean %t\n", *t)
        case *int:
            fmt.Printf("pointer to integer %d\n", *t)
    }
}

Der Type Switch ist eine spezielle Form der switch - case Anweisung. Hier wird nicht der Inhalt, sondern der Typ einer Variablen zum Kriterium gemacht. Die einzelnen Fälle sind skalare Typen des Typsystems von Go.

Der Type Switch ist leistungsfähig, aber nicht hinreichend, um wirklich beliebige Datenstrukturen zu erfassen. Wenn z.B. auch Verbundtypen vorkommen können, muss zusätzlich auf andere Methoden aus der reflect Bibliothek zurückgegriffen werden.

// explizite Interface-Prüfung

type Stringer interface {
    String() string
}

func foo(a interface{}) {
    if _, ok := a.(Stringer); ok {
        fmt.Println("Methode String() vorhanden")
    } else {
        fmt.Println("Methode String() nicht vorhanden")
    }
}

Eine andere, häufig genutzte Reflection Funktion ist die Prüfung, ob ein Typ ein bestimmtes Interface erfüllt. Dazu kann die Schreibweise Variable.(Interface) verwendet werden. Wenn ok anschließend den Wert true hat, erfüllt der Typ das Interface, und die im Interface definierten Methoden können aufgerufen werden.