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.

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")

}
Output
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

Go 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)

}
Output
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

Go 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”.

Go 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)

}
Output
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
/ Weiter

Zurück zu Grundlagen

Zur Übersicht