Kanäle (Channels)

Im vorherigen Abschnitt wurden Goroutinen vorgestellt. Da Goroutinen keine Werte zurückgeben können, stellt sich die Frage nach der Kommunikation zwischen dem Hauptprogramm und den Goroutinen: wie können die Routinen dem Hauptprogramm Werte übergeben? Go stellt hierfür Nachrichtenkanäle (kurz Kanäle, engl. channels) bereit. Kanäle sind Datentypen die wiederum beliebige andere Datentypen übermitteln können. Ein kurzes Beispiel soll das Prinzip von Kanälen verdeutlichen:

package main

import "fmt"

func beispiel(c chan string) {
    c <- "Hallo"
}

func main() {
    c := make(chan string)
    go beispiel(c)
    // blockiert, bis der Wert gelesen wurde:
    text := <-c
    fmt.Println(text)
}

Ein Kanal wird mit der eingebauten Funktion make() erzeugt, in diesem Fall ein ungepufferter Kanal vom Typ string. Den Kanal übergeben wir der Goroutine, die zu irgendeinem Zeitpunkt eine Zeichenkette in den Kanal schreibt (oder »schiebt«, wenn man sich einen Kanal als Röhre vorstellt).

Im Hauptprogramm wird nun aus dem Kanal gelesen. Das Hauptprogramm blockiert so lange, bis ein Wert gelesen werden kann, also in dem Fall, bis die Zeichenkette »Hallo« in der Goroutine in den Kanal geschrieben wird. Erst dann ist der Wert in der Variablen text gespeichert und kann ausgegeben werden. Die letzten beiden Zeilen der main() Funktion könnten auch zusammengefasst werden: fmt.Println(<-c).

Im Beispiel wird ersichtlich, dass Kanäle ihre eigene Syntax haben. In einer Zuweisung kann entweder aus einem Kanal gelesen oder in ein Kanal geschrieben werden. Das zeigt die Position des Zeichens <- an. Steht der Pfeil vor der Variablen, die den Kanal enthält, wird aus ihm gelesen, ansonsten geschrieben. In der Funktion beispiel() wird ein Wert in den Kanal c geschrieben, daher zeigt der Pfeil auf die Variable c. In der Funktion main() wird aus dem Kanal gelesen, daher zeigt der Pfeil von der Kanal- Variablen weg.

Kanäle gibt es drei Ausprägungen: ein Lesekanal, ein Schreibkanal und ein kombinierter Lese-/Schreibkanal. Diese wird durch die Datentypen

  • <-chan
  • chan<-
  • chan

repräsentiert. Man kann einen Lese-/Schreibkanal (chan) in einen der beiden anderen Kanäle wandeln, um dem Empfänger nur bestimmte Operationen zu erlauben:

package main

import "fmt"

func schreibe(c chan<- string) {
    c <- "Hallo"
}

func lese(c <-chan string, e chan<- bool) {
    // blockiert, bis der Wert gelesen wurde
    text := <-c
    fmt.Println(text)
    e <- true
}

func main() {
    c := make(chan string)
    fertig := make(chan bool)
    go lese(c, fertig)
    go schreibe(c)
    <-fertig
}

Dieses Programm erzeugt zwei Kanäle, einen für die Kommunikation zwischen den beiden Goroutinen, schreibe() und lese(), und einen um einen Zustand anzuzeigen. Würden die beiden Goroutinen ohne anschließendes Warten auf den »fertig«-Kanal aufgerufen, tritt wieder das Problem auf, das im Kapitel über Goroutinen beschrieben ist: das Hauptprogramm beendet sich, bevor die Goroutinen ausgeführt werden. Es ist aber für den korrekten Programmablauf notwendig, dass die beiden Funktionen ausgeführt werden. Die Synchronisation wird durch einen einfachen Trick erreicht: die letzte Programmzeile wartet darauf, dass aus dem ungepufferten Kanal fertig ein Wert gelesen werden kann. Der Wert selbst ist nicht wichtig, da er nur als Markierung verwendet wird. Da der Wert am Ende der Funktion lese() in den Kanal geschrieben wird, ist sichergestellt, dass die Funktion lese() vollständig ausgeführt wird. Die Funktion lese() wiederum blockiert so lange, wie kein Wert aus dem Lesekanal c gelesen werden kann. Dadurch wird sichergestellt, dass die Funktion schreibe() ausgeführt wird und einen Wert in den Schreib-Kanal c schreibt.

Warten auf mehrere Kanäle

Manche Operationen muss man vorzeitig beenden, wenn sie zu viel Rechenzeit benötigen. Man könnte beispielsweise Benutzern gestatten auf einem Server Programme ablaufen zu lassen, aber nur bis zu einer bestimmten Maximaldauer. Das Paket time aus der Standardbibliothek bietet hierfür eine praktische Funktion After(), die nach einer vorgegebenen Zeit einen Wert in einen Kanal schreibt. Die Benutzung ist ganz einfach:

package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Vorher")
    timeout := time.After(100 * time.Millisecond)
    <-timeout
    fmt.Println("Nachher")
}

Das Lesen aus dem Kanal, der von der Funktion After() zurückgegeben wird, blockiert die Ausführung bis die Funktion einen Wert in den Kanal schreibt.

Was passiert aber, wenn nicht nur aus dem »Wartekanal«, sondern auch noch aus anderen Kanälen gelesen werden soll? Dann kann die obige Form nicht benutzt werden, weil ja die Programmausführung so lange anhält, wie der Kanal leer ist. Für dieses Problem gibt es die select-Anweisung, die die Möglichkeit bietet, mehrere Kanäle abzufragen. Sobald ein Kanal bereit ist zum lesen, wird der entsprechende Programmteil ausgeführt.

package main

import (
    "fmt"
    "time"
)

func schreibeText(c chan string) {
    time.Sleep(100 * time.Millisecond)
    c <- "Hallo Welt"
}

func main() {
    c := make(chan string)
    timeout := time.After(200 * time.Millisecond)
    go schreibeText(c)
    select {
    case text := <-c:
        fmt.Println("Von Kanal gelesen:", text)
    case <-timeout:
        fmt.Println("Timeout")
    }
}

Das Programm erzeugt zwei Kanäle, einen Kanal vom Typ string und einen Lesekanal timeout, der nach 200 Millisekunden einen Wert erhält. Mithilfe der select-Anweisung können mehrere Kanäle überprüft werden, ob Daten zum Lesen eingetroffen sind. select blockiert so lange, bis eine der Kanäle zum Lesen bereit ist. Sind beide Kanäle bereit, so wählt select einen der Kanäle per Zufall aus. In dem Beispiel wird in den Kanal c nach 100 Millisekunden ein Wert geschrieben, in den Kanal timeout nach 200 Millisekunden. Da die select-Anweisung genau einmal ausgeführt wird, wird nur vom Kanal c gelesen und der andere Kanal wird durch das Programmende nicht weiter gefüllt.

Wie bei der switch-Anweisung gibt es die Möglichkeit, einen »sonst«-Fall anzugeben. Wenn beide Kanäle blockieren (also noch keine Daten bereitstehen), wird dieser Fall ausgeführt.

package main

import (
    "fmt"
    "time"
)

func schreibeText(c chan string) {
    time.Sleep(100 * time.Millisecond)
    c <- "Hallo Welt"
}

func main() {
    c := make(chan string)
    timeout := time.After(200 * time.Millisecond)
    go schreibeText(c)
    for {
        select {
        case text := <-c:
            fmt.Println("Von Kanal gelesen:", text)
        case <-timeout:
            fmt.Println("Timeout")
            return
        default:
            fmt.Println("Ich warte...")
            time.Sleep(10 * time.Millisecond)
        }
    }
}

Hier wird select in einer Endlosschleife (for {}) ausgeführt. Wenn kein Kanal zum Lesen bereit ist, wird der default-Fall ausgeführt. In dem Beispiel wird eine Meldung ausgegeben und 10 Millisekunden gewartet. Probiere aus aus, was passiert, wenn der default-Fall nicht wartet. Nach einiger Zeit wird der Kanal c gefüllt und der erste Fall trifft zu. Nachdem von diesem Kanal gelesen wurde, stehen dort keine Daten mehr bereit und der default-Fall kommt wieder ins Spiel. Das geht so lange weiter bis vom timeout-Kanal Daten empfangen werden, und in dem Fall wird die return-Anweisung ausgeführt und beendet das Programm. Die Ausgabe ist in etwa folgende:

Ich warte...
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Von Kanal gelesen: Hallo Welt
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Ich warte...
Timeout

Schließen von Kanälen

Kanäle können geschlossen werden, wenn man nicht weiter auf sie schreibend zugreifen möchte. Das Schließen mit der close()-Anweisung ist aber nicht zwingend vorgeschrieben und auch nur dann notwendig, wenn der Empfänger diese Information überprüft. Bisher haben wir bei lesenden Zugriffen auf Kanälen nur einen Wert der Leseoperation <- ausgewertet. Es werden aber immer zwei Werte übermittelt, der zweite zeigt an, ob ein Kanal noch offen ist, oder nicht. Wenn man aus einem geschlossenen Kanal liest, werden Null-Werte des jeweiligen Typs gelesen.

package main

import "fmt"

func schreibe(c chan<- string) {
    c <- "Hallo"
    c <- "schöne"
    c <- "Welt"
    close(c)
}

func main() {
    c := make(chan string)
    go schreibe(c)
    for {
        str, ok := <-c
        if ok {
            fmt.Println(str)
        } else {
            return
        }
    }
}

Die for-range-Konstruktion ist auch auf Kanälen definiert und ist sehr praktisch, wenn man bei Datenende den Kanal schließt. Obiges Beispiel kann übersichtlicher geschrieben werden (hier nur die Funktion) main()):

func main() {
    c := make(chan string)
    go schreibe(c)
    for s := range c {
        fmt.Println(s)
    }
}

Gepufferte Kanäle

Dass Kanäle den Programmablauf blockieren können, wurde schon im letzten Abschnitt gezeigt. Hier noch einmal zur Verdeutlichung:

package main

import "fmt"

func main() {
    c := make(chan bool)
    c <- true
    fmt.Println("Hallo")
}

Beim Starten des Programms gibt es einen Laufzeitfehler, weil das Programm »stecken geblieben« ist. Es wird in einen ungepufferten Kanal ein Wert geschrieben. An dieser Stelle wartet Go, dass der Wert aus dem Kanal ausgelesen wird. Da es aber keine laufende Goroutine gibt, die den Wert lesen kann, hängt das Programm in einem sogenannten »Deadlock« (oder Verklemmung). Es geht einfach nicht weiter. Das erkennt das Laufzeitsystem und erzeugt einen schwerwiegenden Fehler:

fatal error: all goroutines are asleep - deadlock!

Manchmal ist es aber gewollt, nach einem Schreiben in ein Kanal weiter zu machen. Das kann sinnvoll sein, um Daten in einem Zwischenspeicher zu »puffern«. Hat man als Datenquelle eine Verbindung, die unterschiedlich schnell Daten erzeugt, möchte man mit dem Einlesen vielleicht nicht so lange warten, bis alle schon geschriebenen Daten verarbeitet wurden. Dazu kann man einen Kanal erzeugen, der mehr als einen Wert aufnehmen kann:

make(chan int, 100)

Ein so erzeugter Kanal kann genau 100 Werte aufnehmen, bevor er blockiert.

package main

import "fmt"

func main() {
    c := make(chan bool, 1)
    c <- true
    fmt.Println("Hallo")
}

Der Kanal c hat eine Kapazität von 1 und blockiert erst, wenn ein weiter Wert in den Kanal geschrieben wird (bitte ausprobieren, es kann ja sein, dass hier Unfug schreibe).