Channels sind ein zentrales Konzept in Go für Nebenläufigkeit. Sie ermöglichen den Datenaustausch zwischen Goroutines und deren Synchronisierung ohne explizite Locks. Dieser Guide behandelt die Verwendung von Channels, häufige Herausforderungen und praktische Patterns für nebenläufige Systeme.
Einführung
Channels in Go ermöglichen es, Daten zwischen Goroutinen sicher und elegant auszutauschen. Das zentrale Motto von Go in Bezug auf Concurrency lautet wie folgt.
Don’t communicate by sharing memory; share memory by communicating. Das bedeutet: Anstatt dass mehrere Goroutinen auf denselben Speicher zugreifen und ihn mit Locks schützen (wie in vielen anderen Sprachen), kommunizieren Goroutinen in Go über Channels. Der Channel selbst kümmert sich um die Synchronisation.
Grundlagen der Concurrency
Goroutinen - die Basis
Bevor wir Channels verstehen können, sollten wir Goroutinen verstehen. Es gibt bereits in einem anderen Artikel “Goroutines” eine ausführliche Beschreibung, was Goroutinen sind. Hier nochmals kurz wiederholend zusammengefasst.
Eine Goroutine ist eine leichtgewichtige Thread-Abstraktion in Go.
package main
import (
"fmt"
"time"
)
func sayHello(name string) {
fmt.Printf("Hello, %s!\n", name)
}
func main() {
// Normale synchrone Ausführung
sayHello("World")
// Startet eine Goroutine
go sayHello("Concurrency")
// Hauptprogramm wartet kurz,
// damit Goroutine Zeit hat
time.Sleep(1 * time.Second)
fmt.Println("Programm Ende")
}Hello, World!
Hello, Concurrency!
Programm EndeWas genau passiert hier?
sayHello("World")wird synchron ausgeführt (normal)go sayHello("Concurrency")startet eine Goroutine- Das Hauptprogramm wartet mit
time.Sleep(), damit Goroutine Zeit hat zu laufen
Ohne Sleep würde das Programm sofort beenden, bevor die Goroutine läuft. Allerdings ist time.Sleep() keine gute Lösung für Synchronisation.
Das Problem ohne Channels
package main
import (
"fmt"
"time"
)
func main() {
counter := 0
// Starte 5 Goroutinen, die den Counter erhöhen
for i := 0; i < 5; i++ {
go func() {
for j := 0; j < 1000; j++ {
counter++ // <=== Race Condition
}
}()
}
time.Sleep(1 * time.Second)
fmt.Printf("Counter: %d\n", counter)
}Counter: 3326Probleme:
- Race Condition: Mehrere Goroutinen greifen gleichzeitig auf
counterzu - Keine Garantie: Wir wissen nicht, wann alle Goroutinen fertig sind
- Unsicher: Das Programm ist nicht deterministisch
Channels lösen beide Probleme.
Was sind Channels?
Ein Channel ist eine typisierte Pipe, durch die Werte gesendet und empfangen werden können. Channels verbinden Goroutinen und ermöglichen sichere Kommunikation.
Anatomie eines Channels
// Channel Deklaration
var ch chan int
// Channels mit make erstellen
ch = make(chan int)
// Oder in einer Zeile
ch := make(chan int)Wichtig
Channels müssen mit make erstellt werden. Ein deklarierter aber nicht initialisierter Channel hat den Wert nil.
Drei grundlegenden Channel-Operationen
Hierbei handelt sich um Operationen: “Channel erstellen”, “Daten senden”, “Daten empfangen”.
package main
import "fmt"
func main() {
// 1. Channel erstellen
ch := make(chan string)
// 2. Daten senden
go func() {
ch <- "Hallo aus Goroutine"
}()
// 3. Daten empfangen
msg := <-ch
fmt.Println(msg)
}Hallo aus GoroutineDie Operatoren
ch <- value: Sendevaluein den Channelchvalue := <- ch: Empfange einen Wert aus Channelch<-ch: Empfange und verwerfe diesen Wert