Verwenden von Kanälen als Kommunikationsmechanismus

Abgeschlossen

Ein Kanal in Go ist ein Kommunikationsmechanismus zwischen Goroutinen. Denken Sie daran, dass der Ansatz von Go für Nebenläufigkeit lautet: „Nicht kommunizieren durch Teilen von Arbeitsspeicher, sondern Teilen von Arbeitsspeicher durch Kommunizieren“. Wenn Sie einen Wert von einer Go-Routine an eine andere senden müssen, verwenden Sie Kanäle. Sehen wir uns nun an, wie sie funktionieren und zum Schreiben gleichzeitiger Go-Programme verwendet werden können.

Kanalsyntax

Da ein Kanal ein Kommunikationsmechanismus ist, der Daten sendet und empfängt, weist er auch einen Typ auf. Das bedeutet, dass Sie nur Daten für den vom Kanal unterstützten Typ senden können. Sie verwenden das Schlüsselwort chan als Datentyp für einen Kanal, aber Sie müssen auch den Datentyp angeben, der über den Kanal übergeben wird, z. B. den Typ int.

Jedes Mal, wenn Sie einen Kanal deklarieren oder einen Kanal als Parameter in einer Funktion angeben möchten, müssen Sie chan <type> verwenden, z. B. chan int. Zum Erstellen eines Kanals verwenden Sie die integrierte make()-Funktion:

ch := make(chan int)

Ein Kanal kann zwei Vorgänge ausführen: Daten senden und Daten empfangen. Um den Vorgangstyp eines Kanals anzugeben, müssen Sie den Kanaloperator <- verwenden. Außerdem sind das Senden von Daten und das Empfangen von Daten in Kanälen blockierende Vorgänge. Sie werden gleich sehen, warum das so ist.

Wenn Sie angeben möchten, dass ein Kanal nur Daten sendet, müssen Sie den Operator <- hinter dem Kanal verwenden. Wenn der Kanal Daten empfangen soll, verwenden Sie den Operator <- vor dem Kanal, wie in diesem Beispiel:

ch <- x // sends (or writes ) x through channel ch
x = <-ch // x receives (or reads) data sent to the channel ch
<-ch // receives data, but the result is discarded

Ein weiterer Vorgang, den Sie in einem Kanal verwenden können, ist das Schließen. Zum Schließen eines Kanals verwenden Sie die integrierte close()-Funktion:

close(ch)

Wenn Sie einen Kanal schließen, geben Sie an, dass keine Daten mehr in diesem Kanal gesendet werden sollen. Wenn Sie versuchen, Daten an einen geschlossenen Kanal zu senden, wird im Programm die Panic-Funktion genutzt. Wenn Sie versuchen, Daten von einem geschlossenen Kanal zu empfangen, können Sie alle gesendeten Daten lesen. Jeder nachfolgende Lesevorgang gibt dann einen Nullwert zurück.

Wir kehren nun zu dem zuvor erstellten Programm zurück und verwenden Kanäle, um die sleep-Funktion zu entfernen. Zuerst wird auf folgende Weise ein Zeichenfolgenkanal in der main-Funktion erstellt:

ch := make(chan string)

Dann wird die Zeile für den Ruhezustand time.Sleep(3 * time.Second) entfernt.

Nun können Kanäle für die Kommunikation zwischen Goroutinen verwendet werden. Anstatt das Ergebnis in der checkAPI-Funktion auszugeben, können wir den Code umgestalten und diese Meldung über den Kanal senden. Wenn Sie den Kanal aus dieser Funktion verwenden möchten, müssen Sie den Kanal als Parameter hinzufügen. Die checkAPI-Funktion sollte wie folgt aussehen:

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

Beachten Sie, dass wir die fmt.Sprintf-Funktion verwenden müssen, da wir keinen Text ausgeben, sondern nur formatierten Text über den Kanal senden möchten. Beachten Sie auch, dass der Operator <- hinter der Kanalvariablen verwendet wird, um Daten zu senden.

Nun müssen Sie die main-Funktion wie folgt ändern, damit die Kanalvariable gesendet und die Daten zur Ausgabe empfangen werden:

ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)

Beachten Sie, wie durch Platzierung des Operators <- vor dem Kanal angegeben wird, dass Daten aus dem Kanal gelesen werden sollen.

Wenn Sie das Programm erneut ausführen, wird eine Ausgabe wie die folgende angezeigt:

ERROR: https://api.somewhereintheinternet.com/ is down!

Done! It took 0.007401217 seconds!

Dies funktioniert zumindest ohne Aufrufen einer Ruhezustandsfunktion. Aber es werden noch immer nicht die gewünschten Vorgänge ausgeführt. Es wird nur die Ausgabe einer der Goroutinen angezeigt, aber wir haben fünf erstellt. Im nächsten Abschnitt wird erläutert, warum das Programm auf diese Weise funktioniert.

Nicht gepufferte Kanäle

Wenn Sie einen Kanal mit der make()-Funktion erstellen, wird ein nicht gepufferter Kanal erstellt. Dies ist das Standardverhalten. Nicht gepufferte Kanäle blockieren den Sendevorgang, bis die Daten empfangen werden können. Wie zuvor bereits angemerkt, sind das Senden und Empfangen blockierende Vorgänge. Dieser blockierende Vorgang ist auch der Grund, warum das Programm aus dem vorherigen Abschnitt beendet wurde, sobald die erste Meldung empfangen wurde.

Wir können zunächst einmal sagen, dass fmt.Print(<-ch) das Programm blockiert, weil aus einem Kanal gelesen und auf das Eintreffen von Daten gewartet wird. Sobald Daten eingetroffen sind, wird mit der nächsten Zeile fortgefahren, und das Programm wird beendet.

Was ist mit den restlichen Goroutinen geschehen? Sie werden weiterhin ausgeführt, doch es wird nicht mehr darauf gelauscht. Außerdem konnten durch das frühzeitige Beenden des Programms einige Goroutinen keine Daten senden. Um das zu beweisen, fügen wir eine weitere fmt.Print(<-ch)-Zeile wie folgt hinzu:

ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
fmt.Print(<-ch)

Wenn Sie das Programm erneut ausführen, wird eine Ausgabe wie die folgende angezeigt:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
Done! It took 0.263611711 seconds!

Beachten Sie, dass jetzt die Ausgabe für zwei APIs angezeigt wird. Wenn Sie noch weitere fmt.Print(<-ch)-Zeilen hinzufügen, können Sie schließlich alle Daten lesen, die an den Kanal gesendet werden. Doch was geschieht, wenn Sie versuchen, noch mehr Daten zu lesen, und keine Daten mehr gesendet werden? Ein Beispiel dafür sieht etwa wie folgt aus:

ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)

fmt.Print(<-ch)

Wenn Sie das Programm erneut ausführen, wird eine Ausgabe wie die folgende angezeigt:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://dev.azure.com is up and running!

Es funktioniert, aber das Programm wird nicht beendet. Die letzte Ausgabezeile blockiert das Programm, da der Empfang von Daten erwartet wird. Sie müssen das Programm mit einem Befehl wie Ctrl+C schließen.

Das vorherige Beispiel beweist lediglich, dass das Lesen von Daten und das Empfangen von Daten blockierende Vorgänge sind. Um dieses Problem zu beheben, könnten Sie den Code in eine for-Schleife ändern und nur die Daten empfangen, von denen Sie sicher wissen, dass sie gesendet werden, wie in diesem Beispiel:

for i := 0; i < len(apis); i++ {
    fmt.Print(<-ch)
}

Hier sehen Sie die endgültige Version des Programms, falls bei Ihrer Version etwas schief gelaufen ist:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    ch := make(chan string)

    for _, api := range apis {
        go checkAPI(api, ch)
    }

    for i := 0; i < len(apis); i++ {
        fmt.Print(<-ch)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

Wenn Sie das Programm erneut ausführen, wird eine Ausgabe wie die folgende angezeigt:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
Done! It took 0.602099714 seconds!

Das Programm funktioniert erwartungsgemäß. Sie verwenden keine sleep-Funktion mehr, sondern Sie verwenden Kanäle. Beachten Sie außerdem, dass es jetzt ungefähr 600 ms dauert, bis das Programm beendet wird, anstatt fast 2 Sekunden, als wir keine Nebenläufigkeit verwendet haben.

Abschließend kann noch gesagt werden, dass nicht gepufferte Kanäle die Sende- und Empfangsvorgänge synchronisieren. Obwohl Sie Nebenläufigkeit verwenden, erfolgt die Kommunikation synchron.