Kanalprimitiven in Golang

GolangGolangBeginner
Jetzt üben

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Eines der attraktivsten Merkmale der Programmiersprache Go ist ihre native Unterstützung für nebenläufige Programmierung. Sie bietet starke Unterstützung für Parallelität, nebenläufige Programmierung und Netzwerkkommunikation, was eine effizientere Nutzung von Mehrkernprozessoren ermöglicht.

Im Gegensatz zu anderen Programmiersprachen, die Nebenläufigkeit durch geteilten Speicher erreichen, realisiert Go Nebenläufigkeit durch die Verwendung von Kanälen (channels) für die Kommunikation zwischen verschiedenen Goroutinen.

Goroutinen sind die Einheiten der Nebenläufigkeit in Go und können als leichte Threads angesehen werden. Sie verbrauchen im Vergleich zu traditionellen Threads weniger Ressourcen beim Wechseln zwischen ihnen.

Kanäle und Goroutinen bilden zusammen die Grundelemente der Nebenläufigkeit in Go. In diesem Abschnitt werden wir uns mit Kanälen, dieser neuen Datenstruktur, vertraut machen.

Wissenspunkte:

  • Arten von Kanälen
  • Erstellen von Kanälen
  • Operationen auf Kanälen
  • Kanalblockierung
  • Einwegkanäle

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL go(("Golang")) -.-> go/BasicsGroup(["Basics"]) go(("Golang")) -.-> go/DataTypesandStructuresGroup(["Data Types and Structures"]) go(("Golang")) -.-> go/ConcurrencyGroup(["Concurrency"]) go/BasicsGroup -.-> go/values("Values") go/BasicsGroup -.-> go/variables("Variables") go/DataTypesandStructuresGroup -.-> go/structs("Structs") go/ConcurrencyGroup -.-> go/goroutines("Goroutines") go/ConcurrencyGroup -.-> go/channels("Channels") go/ConcurrencyGroup -.-> go/select("Select") subgraph Lab Skills go/values -.-> lab-149096{{"Kanalprimitiven in Golang"}} go/variables -.-> lab-149096{{"Kanalprimitiven in Golang"}} go/structs -.-> lab-149096{{"Kanalprimitiven in Golang"}} go/goroutines -.-> lab-149096{{"Kanalprimitiven in Golang"}} go/channels -.-> lab-149096{{"Kanalprimitiven in Golang"}} go/select -.-> lab-149096{{"Kanalprimitiven in Golang"}} end

Überblick über Kanäle (Channels)

Kanäle (Channels) sind eine spezielle Datenstruktur, die hauptsächlich für die Kommunikation zwischen Goroutinen verwendet wird.

Im Gegensatz zu anderen Nebenläufigkeitsmodellen, die Mutexe und atomare Funktionen für den sicheren Zugriff auf Ressourcen nutzen, ermöglichen Kanäle die Synchronisierung durch das Senden und Empfangen von Daten zwischen mehreren Goroutinen.

In Go verhält sich ein Kanal (Channel) wie ein Förderband oder eine Warteschlange, die der First-In-First-Out (FIFO)-Regel folgt.

Kanaltypen (Channel Types) und Deklaration

Ein Kanal (Channel) ist ein Referenztyp, und seine Deklarationsform ist wie folgt:

var channelName chan elementType

Jeder Kanal hat einen bestimmten Typ und kann nur Daten desselben Typs transportieren. Sehen wir uns einige Beispiele an:

Erstellen Sie eine Datei namens channel.go im Verzeichnis ~/project:

cd ~/project
touch channel.go

Beispielsweise deklarieren wir einen Kanal (Channel) vom Typ int:

var ch chan int

Der obige Code deklariert einen int-Kanal (Channel) namens ch.

Darüber hinaus gibt es viele andere häufig verwendete Kanaltypen (Channel Types).

Schreiben Sie in der Datei channel.go folgenden Code:

package main

import "fmt"

func main() {
    var ch1 chan int   // Declare an integer channel
    var ch2 chan bool  // Declare a boolean channel
    var ch3 chan []int // Declare a channel that transports slices of integers
    fmt.Println(ch1, ch2, ch3)
}

Der obige Code deklariert einfach drei verschiedene Kanaltypen (Channel Types).

Führen Sie das Programm mit dem folgenden Befehl aus:

go run channel.go

Die Ausgabe des Programms ist wie folgt:

<nil> <nil> <nil>

Kanalinitialisierung (Channel Initialization)

Ähnlich wie bei Maps müssen Kanäle (Channels) nach der Deklaration initialisiert werden, bevor sie verwendet werden können.

Die Syntax zur Initialisierung eines Kanals (Channel) lautet wie folgt:

make(chan elementType)

Schreiben Sie in der Datei channel.go folgenden Code:

package main

import "fmt"

func main() {
    // Channel that stores integer data
    ch1 := make(chan int)

    // Channel that stores boolean data
    ch2 := make(chan bool)

    // Channel that stores []int data
    ch3 := make(chan []int)

    fmt.Println(ch1, ch2, ch3)
}

Führen Sie das Programm mit dem folgenden Befehl aus:

go run channel.go

Die Ausgabe des Programms ist unten gezeigt:

0xc0000b4000 0xc0000b4040 0xc0000b4080

Im obigen Code werden drei verschiedene Kanaltypen (Channel Types) definiert und initialisiert.

Durch die Untersuchung der Ausgabe können wir feststellen, dass, im Gegensatz zu Maps, das direkte Drucken des Kanals (Channel) selbst dessen Inhalt nicht preisgibt.

Das direkte Drucken eines Kanals (Channel) gibt nur die Speicheradresse des Kanals selbst zurück. Dies liegt daran, dass ein Kanal (Channel), wie ein Zeiger, nur eine Referenz auf eine Speicheradresse ist.

Wie können wir also auf die Werte in einem Kanal (Channel) zugreifen und damit arbeiten?

Kanaloperationen (Channel Operations)

Datensenden

Kanaloperationen (Channel Operations) haben ihre eigenen einzigartigen Methoden. Im Allgemeinen verwenden wir bei der Arbeit mit anderen Datentypen = und Indizes, um auf die Daten zuzugreifen und sie zu manipulieren.

Bei Kanälen (Channels) verwenden wir jedoch <- sowohl für das Senden als auch für das Empfangen von Daten.

In einem Kanal (Channel) wird das Schreiben von Daten als Senden bezeichnet, was bedeutet, dass ein Datenelement in den Kanal gesendet wird.

Wie senden wir also Daten?

Schreiben Sie in der Datei channel.go folgenden Code:

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 10
    fmt.Println(ch)
}

Führen Sie das Programm mit dem folgenden Befehl aus:

go run channel.go

Die Ausgabe des Programms ist wie folgt:

0xc0000b4000

Obwohl wir Daten in den Kanal gelegt haben, können wir sie nicht direkt aus der Ausgabe des Kanalwerts sehen.

Dies liegt daran, dass die Daten im Kanal noch empfangen werden müssen, bevor sie verwendet werden können.

Datenempfang

Was bedeutet es, Daten aus einem Kanal (Channel) zu empfangen? Wenn wir uns mit Maps oder Slices befassen, können wir beliebige Daten aus diesen Datentypen anhand von Indizes oder Schlüsseln abrufen.

Bei Kanälen (Channels) können wir nur den frühesten Wert abrufen, der in den Kanal eingetreten ist.

Nachdem der Wert empfangen wurde, existiert er nicht mehr im Kanal.

Mit anderen Worten, das Empfangen von Daten aus einem Kanal (Channel) ist wie das Abfragen und Löschen von Daten in anderen Datentypen.

Schreiben Sie in der Datei channel.go folgenden Code:

package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Führen Sie das Programm mit dem folgenden Befehl aus:

go run channel.go

Die Ausgabe des Programms ist wie folgt:

10
20

Wir können sehen, dass der Wert 10, der zuerst in den Kanal eingetreten ist, zuerst ausgegeben wird, und nachdem er ausgegeben wurde, existiert er nicht mehr im Kanal.

Der später eingetretene Wert 20 wird später ausgegeben, was das First-In-First-Out-Prinzip des Kanals (Channel) verdeutlicht.

Kanal schließen

Wenn ein Kanal (Channel) nicht mehr benötigt wird, sollte er geschlossen werden. Bei der Schließung eines Kanals müssen jedoch die folgenden Punkte beachtet werden:

  • Das Senden eines Werts an einen geschlossenen Kanal (Channel) führt zu einem Laufzeitfehler.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
        ch <- 10
    
        fmt.Println(<-ch)
    
        close(ch)
        ch <- 20
    }

    Die Ausgabe des Programms ist wie folgt:

    10
    panic: send on closed channel
    
    goroutine 1 [running]:
    main.main()
        /home/labex/project/channel.go:10 +0x1a0
    exit status 2
  • Das Empfangen von Daten aus einem geschlossenen Kanal (Channel) setzt den Abruf von Werten fort, bis der Kanal leer ist.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
        ch <- 30
    
        close(ch)
    
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
    }

    Führen Sie das Programm mit dem folgenden Befehl aus:

    go run channel.go

    Die Ausgabe des Programms ist wie folgt:

    10
    20
    30
  • Das Empfangen von Daten aus einem geschlossenen, leeren Kanal (Channel) liefert den Standard-Initialwert des entsprechenden Datentyps.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
    
        ch <- 10
        ch <- 20
        ch <- 30
    
        close(ch)
    
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
        fmt.Println(<-ch)
    }

    Führen Sie das Programm mit dem folgenden Befehl aus:

    go run channel.go

    Die Ausgabe des Programms ist wie folgt:

    10
    20
    30
    0
    0
  • Das Schließen eines bereits geschlossenen Kanals (Channel) löst einen Fehler aus.

    package main
    
    import "fmt"
    
    func main() {
        ch := make(chan int, 3)
        ch <- 10
        close(ch)
        close(ch)
        fmt.Println(<-ch)
    }

    Die Ausgabe des Programms ist wie folgt:

    panic: close of closed channel
    
    goroutine 1 [running]:
    main.main()
        /home/labex/project/channel.go:9 +0x75
    exit status 2

Kanalblockierung (Channel Blocking)

Aufmerksame Schülerinnen und Schüler haben vielleicht bemerkt, dass wir bei der Einführung der Kanaldeklaration (Channel Declaration) und -initialisierung (Channel Initialization) die Kapazität des Kanals (Channel) nicht angegeben haben:

package main

import "fmt"

func main() {
    // Stores integer data in the channel
    ch1 := make(chan int) // Same below

    // Stores boolean data in the channel
    ch2 := make(chan bool)

    // Stores []int data in the channel
    ch3 := make(chan []int)

    fmt.Println(ch1, ch2, ch3)
}

Aber als wir die Kanaloperationen (Channel Operations) demonstrierten, haben wir die Kapazität des Kanals (Channel) angegeben:

package main

import "fmt"

func main() {
    // Specify the channel capacity as 3
    ch := make(chan int, 3)
    ch <- 10
    ch <- 20
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

Für Kanäle (Channels), deren Kapazität nicht angegeben ist, werden sie als ungebufferte Kanäle (unbuffered channels) bezeichnet, die keinen Pufferplatz haben.

Wenn ein Sender oder Empfänger nicht bereit ist, blockiert die erste Operation, bis die andere Operation bereit ist.

chan1 := make(chan int) // Unbuffered channel of type int

Für Kanäle (Channels), deren Kapazität und Pufferplatz angegeben sind, werden sie als gepufferte Kanäle (buffered channels) bezeichnet.

chan2 := make(chan int, 5) // Buffered channel of type int with a capacity of 5

Sie funktionieren wie eine Warteschlange und folgen der First-In-First-Out (FIFO)-Regel.

Bei gepufferten Kanälen (buffered channels) können wir Sendevorgänge verwenden, um Elemente an das Ende der Warteschlange anzufügen, und Empfangsvorgänge verwenden, um Elemente vom Anfang der Warteschlange zu entfernen.

Was passiert, wenn wir mehr Daten in einen gepufferten Kanal (buffered channel) legen, als seine Kapazität beträgt? Lassen Sie uns es überprüfen. Schreiben Sie folgenden Code in die Datei channel1.go:

cd ~/project
touch channel1.go
package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 10
    ch <- 20
    fmt.Println("succeed")
}

Führen Sie das Programm mit dem folgenden Befehl aus:

go run channel1.go

Die Ausgabe des Programms ist wie folgt:

fatal error: all goroutines are asleep - deadlock!

Wir stellen fest, dass das Programm in einem Deadlock endet, weil der Kanal bereits voll ist.

Um dieses Problem zu lösen, können wir zuerst die Daten aus dem Kanal extrahieren. Schreiben Sie folgenden Code in channel.go:

package main

import "fmt"

func main() {
    ch := make(chan int, 1)
    ch <- 10
    fmt.Println("Data extracted:", <-ch)

    ch <- 20
    fmt.Println("succeed")
}

Führen Sie das Programm mit dem folgenden Befehl aus:

go run channel.go

Die Ausgabe des Programms ist wie folgt:

Data extracted: 10
succeed

Wir können den Kanal mit begrenzter Kapazität kontinuierlich nutzen, indem wir eine Strategie von Entnehmen, Verwenden, Entnehmen und Wiederverwenden anwenden.

Einwegkanäle (Unidirectional Channels)

Standardmäßig ist ein Kanal (Channel) bidirektional und kann sowohl gelesen als auch geschrieben werden. Manchmal müssen wir die Verwendung eines Kanals einschränken, z. B. möchten wir sicherstellen, dass ein Kanal innerhalb einer Funktion nur geschrieben oder nur gelesen werden kann. Wie können wir dies erreichen?

Wir können einen Kanal mit eingeschränkter Verwendung explizit deklarieren, bevor wir ihn initialisieren.

Schreibgeschützter Kanal (Write-only Channel)

Ein schreibgeschützter Kanal (write-only channel) bedeutet, dass innerhalb einer bestimmten Funktion der Kanal nur geschrieben, aber nicht gelesen werden kann. Lassen Sie uns einen schreibgeschützten Kanal vom Typ int deklarieren und verwenden:

package main

import "fmt"

func writeOnly(ch chan<- int) {
    ch <- 10
    fmt.Println("Data written to channel")
}

func main() {
    ch := make(chan int, 1)
    writeOnly(ch)
    fmt.Println("Data received:", <-ch)
}

Führen Sie das Programm mit dem folgenden Befehl aus:

go run channel.go

Die Ausgabe des Programms ist wie folgt:

Data written to channel
Data received: 10

In diesem Beispiel schränkt die Funktion writeOnly den Kanal auf Schreibzugriffe ein.

Lesegeschützter Kanal (Read-only Channel)

Ein lesegeschützter Kanal (read-only channel) bedeutet, dass innerhalb einer bestimmten Funktion der Kanal nur gelesen, aber nicht geschrieben werden kann. Lassen Sie uns einen lesegeschützten Kanal vom Typ int deklarieren und verwenden:

package main

import "fmt"

func readOnly(ch <-chan int) {
    fmt.Println("Data received from channel:", <-ch)
}

func main() {
    ch := make(chan int, 1)
    ch <- 20
    readOnly(ch)
}

Führen Sie das Programm mit dem folgenden Befehl aus:

go run channel.go

Die Ausgabe des Programms ist wie folgt:

Data received from channel: 20

In diesem Beispiel schränkt die Funktion readOnly den Kanal auf Lesezugriffe ein.

Kombination von schreib- und lesegeschützten Kanälen

Sie können schreib- und lesegeschützte Kanäle kombinieren, um zu zeigen, wie Daten durch eingeschränkte Kanäle fließen:

package main

import "fmt"

func writeOnly(ch chan<- int) {
    ch <- 30
    fmt.Println("Data written to channel")
}

func readOnly(ch <-chan int) {
    fmt.Println("Data received from channel:", <-ch)
}

func main() {
    ch := make(chan int, 1)
    writeOnly(ch)
    readOnly(ch)
}

Führen Sie das Programm mit dem folgenden Befehl aus:

go run channel.go

Die Ausgabe des Programms ist wie folgt:

Data written to channel
Data received from channel: 30

Dieses Beispiel zeigt, wie ein schreibgeschützter Kanal Daten an einen lesegeschützten Kanal übergeben kann.

Zusammenfassung

In diesem Lab haben wir die Grundlagen von Kanälen (Channels) gelernt, die Folgendes umfassen:

  • Arten von Kanälen (Channels) und wie man sie deklariert
  • Methoden zur Initialisierung von Kanälen (Channels)
  • Operationen auf Kanälen (Channels)
  • Das Konzept der Kanalblockierung (Channel Blocking)
  • Die Deklaration von Einwegkanälen (Unidirectional Channels)