Von Goroutine zu Channel: Go's CSP-Modell verstehen
Lukas Schneider
DevOps Engineer · Leapcell

Vorwort
Die Implementierung von Go's CSP-Concurrency-Modell besteht aus zwei Hauptkomponenten: der Goroutine und dem Channel. Dieser Artikel stellt ihre grundlegende Verwendung und die wichtigsten Punkte vor, die zu beachten sind.
Goroutine
Eine Goroutine ist die grundlegende Ausführungseinheit einer Go-Anwendung. Es handelt sich um einen schlanken User-Level-Thread, dessen zugrunde liegende Implementierung von Concurrency auf Coroutinen basiert. Bekanntermaßen sind Coroutinen User-Threads, die im User-Modus laufen; daher werden Goroutinen auch von der Go-Runtime geplant.
Grundlegende Verwendung
Syntax:
go
+ Funktion/Methode
Sie können eine Goroutine erstellen, indem Sie das Schlüsselwort go
gefolgt von einer Funktion/Methode verwenden.
Beispielcode:
import ( "fmt" "time" ) func printGo() { fmt.Println("Benannte Funktion") } type G struct { } func (g G) g() { fmt.Println("Methode") } func main() { // Goroutine aus benannter Funktion erstellen go printGo() // Goroutine aus Methode erstellen g := G{} go g.g() // Goroutine aus anonymer Funktion erstellen go func() { fmt.Println("Anonyme Funktion") }() // Goroutine aus Closure erstellen i := 0 go func() { i++ fmt.Println("Closure") }() time.Sleep(time.Second) // Verhindern, dass die Haupt-Goroutine endet, bevor die erstellten Goroutinen eine Chance haben, ausgeführt zu werden; daher 1 Sekunde warten }
Ausführungsergebnis:
Benannte Funktion
Methode
Anonyme Funktion
Wenn mehrere Goroutinen vorhanden sind, ist ihre Ausführungsreihenfolge nicht festgelegt. Daher variieren die gedruckten Ergebnisse jedes Mal.
Wie aus dem Code ersichtlich, können wir mit dem Schlüsselwort go
Goroutinen basierend auf benannten Funktionen oder Methoden sowie anonymen Funktionen oder Closures erstellen.
Wie wird eine Goroutine beendet? Normalerweise wird sie beendet, sobald die Funktion der Goroutine ausgeführt wurde oder zurückkehrt. Wenn die Funktion oder Methode in der Goroutine einen Rückgabewert hat, wird dieser beim Beenden der Goroutine ignoriert.
Channel
Channel spielen eine wichtige Rolle im Concurrency-Modell von Go. Sie können für die Kommunikation zwischen Goroutinen und für die Synchronisation zwischen Goroutinen verwendet werden.
Grundlegende Operationen von Channel
Ein Channel ist ein zusammengesetzter Datentyp, und bei der Deklaration müssen Sie den Typ der Elemente angeben, die er speichern soll.
Deklarationssyntax:
var ch chan string
Der obige Code deklariert einen Channel, dessen Elementtyp string
ist, was bedeutet, dass er nur string
-Werte speichern kann. Ein Channel ist ein Referenztyp und muss initialisiert werden, bevor Daten hineingeschrieben werden können. Er wird mit make
initialisiert.
import ( "fmt" ) func main() { var ch chan string ch = make(chan string, 1) // Adresse des Channels ausgeben fmt.Println(ch) // "Go" in ch senden ch <- "Go" // Daten von ch empfangen s := <-ch fmt.Println(s) // Go }
Sie können Daten mit ch <- xxx
in eine Channel-Variable ch
senden und mit x := <-ch
Daten daraus empfangen.
Gepufferte vs. ungepufferte Channel
Wenn Sie beim Initialisieren eines Channels keine Kapazität angeben, wird ein ungepufferter Channel erstellt:
ch := make(chan string)
In einem ungepufferten Channel sind Sende- und Empfangsoperationen synchron. Nach der Ausführung einer Sendeoperation blockiert die entsprechende Goroutine, bis eine andere Goroutine eine Empfangsoperation ausführt, und umgekehrt. Was passiert, wenn die Sende- und Empfangsoperationen in derselben Goroutine platziert werden? Sehen wir uns den folgenden Code an:
import ( "fmt" ) func main() { ch := make(chan int) // Daten senden ch <- 1 // fatal error: all goroutines are asleep - deadlock! // Daten empfangen n := <-ch fmt.Println(n) }
Wenn das Programm ausgeführt wird, wird ein schwerwiegender Fehler bei ch <-
ausgegeben, der besagt, dass alle Goroutinen schlafen - mit anderen Worten, es ist ein Deadlock aufgetreten. Um dies zu vermeiden, müssen wir die Sende- und Empfangsoperationen in verschiedenen Goroutinen platzieren.
import ( "fmt" ) func main() { ch := make(chan int) go func() { // Daten senden ch <- 1 }() // Daten empfangen n := <-ch fmt.Println(n) // 1 }
Aus dem obigen Beispiel können wir schließen, dass für ungepufferte Channel die Sende- und Empfangsoperationen in zwei verschiedenen Goroutinen ausgeführt werden müssen; andernfalls tritt ein Deadlock auf.
Wenn Sie eine Kapazität angeben, wird ein gepufferter Channel erstellt:
ch := make(chan string, 5)
Gepufferte Channel unterscheiden sich von ungepufferten Channel: Bei der Ausführung einer Sendeoperation wird die Goroutine nicht unterbrochen, solange der Puffer des Channels nicht voll ist. Nur wenn der Puffer voll ist, führt das Senden an den Channel dazu, dass die Goroutine unterbrochen wird. Beispielcode:
func main() { ch := make(chan int, 1) // Daten senden ch <- 1 ch <- 2 // fatal error: all goroutines are asleep - deadlock! }
Deklarieren von Nur-Sende- und Nur-Empfangs-Channel
Channel, die sowohl senden als auch empfangen können
ch := make(chan int, 1)
Mit dem obigen Code erhalten wir eine Channel-Variable, auf der wir sowohl Sende- als auch Empfangsoperationen ausführen können.
Nur-Empfangs-Channel
ch := make(<-chan int, 1)
Mit dem obigen Code erhalten wir eine Channel-Variable, auf der wir nur Empfangsoperationen ausführen können.
Nur-Sende-Channel
ch := make(chan<- int, 1)
Mit dem obigen Code erhalten wir eine Channel-Variable, auf der wir nur Sendeoperationen ausführen können.
Normalerweise werden Nur-Sende- und Nur-Empfangs-Channel-Typen als Funktionsparametertypen oder Rückgabewerte verwendet:
func send(ch chan<- int) { ch <- 1 } func recv(ch <-chan int) { <-ch }
Schließen eines Channels
Sie können einen Channel mit der eingebauten Funktion close(c chan<- Type)
schließen.
Schließen eines Channels auf der Sendeseite Nachdem ein Channel geschlossen wurde, können Sie keine Sendeoperationen mehr darauf ausführen; andernfalls tritt ein Panic auf, der anzeigt, dass der Channel bereits geschlossen ist.
func main() { ch := make(chan int, 5) ch <- 1 close(ch) ch <- 2 // panic: send on closed channel }
Nachdem ein Channel geschlossen wurde, können Sie weiterhin Empfangsoperationen darauf ausführen. Wenn der Channel einen Puffer hat, werden die gepufferten Daten zuerst ausgelesen. Wenn der Puffer leer ist, ist der abgerufene Wert der Nullwert des Elementtyps des Channels.
import "fmt" func main() { ch := make(chan int, 5) ch <- 1 close(ch) fmt.Println(<-ch) // 1 n, ok := <-ch fmt.Println(n) // 0 fmt.Println(ok) // false }
Beim Durchlaufen eines Channels mit for-range
wird die for-range
-Schleife beendet, wenn der Channel während der Iteration geschlossen wird.
Zusammenfassung
Dieser Artikel hat zunächst vorgestellt, wie man Goroutinen erstellt und unter welchen Bedingungen sie beendet werden.
Anschließend wurde beschrieben, wie man Channel-Variablen erstellt, sowohl gepufferte als auch ungepufferte. Es ist wichtig zu beachten, dass für ungepufferte Channel die Sende- und Empfangsoperationen in zwei verschiedenen Goroutinen ausgeführt werden müssen; andernfalls tritt ein Fehler auf.
Als Nächstes wurde erläutert, wie man Nur-Sende- und Nur-Empfangs-Channel-Typen definiert. Typischerweise werden diese Typen als Funktionsparametertypen oder Rückgabewerte verwendet.
Schließlich wurde behandelt, wie man einen Channel schließt und welche Vorsichtsmaßnahmen nach dem Schließen zu beachten sind.
Wir sind Leapcell, Ihre beste Wahl für das Hosten von Go-Projekten.
Leapcell ist die Serverless-Plattform der nächsten Generation für Webhosting, asynchrone Aufgaben und Redis:
Multi-Sprachen-Unterstützung
- Entwickeln Sie mit Node.js, Python, Go oder Rust.
Stellen Sie unbegrenzt Projekte kostenlos bereit
- Zahlen Sie nur für die Nutzung - keine Anfragen, keine Gebühren.
Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützt 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollständig automatisierte CI/CD-Pipelines und GitOps-Integration.
- Echtzeitmetriken und -protokollierung für verwertbare Erkenntnisse.
Mühelose Skalierbarkeit und hohe Leistung
- Auto-Skalierung zur einfachen Bewältigung hoher Parallelität.
- Null Betriebsaufwand - konzentrieren Sie sich einfach auf das Erstellen.
Erfahren Sie mehr in der Dokumentation!
Folgen Sie uns auf X: @LeapcellHQ