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:
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.
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).