navigation Navigation


Inhaltsverzeichnis

Channels


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.

Inhaltsverzeichnis

    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.

    Beispiel - Goroutine
    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 Ende

    Was 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

    Beispiel - 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: 3326

    Probleme:

    • Race Condition: Mehrere Goroutinen greifen gleichzeitig auf counter zu
    • 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

    Anatomie
    // 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”.

    Beispiel - Operationen
    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 Goroutine

    Die Operatoren

    • ch <- value: Sende value in den Channel ch
    • value := <- ch: Empfange einen Wert aus Channel ch
    • <-ch: Empfange und verwerfe diesen Wert