Använda kanaler som en kommunikationsmekanism

Slutförd

En kanal i Go är en kommunikationsmekanism mellan goroutines. Kom ihåg att Gos metod för samtidighet är: "Kommunicera inte genom att dela minne, dela i stället minne genom att kommunicera." När du behöver skicka ett värde från en goroutine till en annan använder du kanaler. Nu ska vi se hur de fungerar och hur du kan börja använda dem för att skriva samtidiga Go-program.

Kanalsyntax

Eftersom en kanal är en kommunikationsmekanism som skickar och tar emot data har den också en typ. Det innebär att du bara kan skicka data för den typ som kanalen stöder. Du använder nyckelordet chan som datatyp för en kanal, men du måste också ange den datatyp som ska passera genom kanalen, som en int typ.

Varje gång du deklarerar en kanal eller vill ange en kanal som en parameter i en funktion måste du använda chan <type>, till exempel chan int. Om du vill skapa en kanal använder du den inbyggda make() funktionen:

ch := make(chan int)

En kanal kan utföra två åtgärder: skicka data och ta emot data. Om du vill ange vilken typ av åtgärd som en kanal har måste du använda kanaloperatorn <-. Att skicka data och ta emot data i kanaler blockerar dessutom åtgärder. Du kommer snart att se varför.

När du vill säga att en kanal bara skickar data använder du operatorn <- efter kanalen. När du vill att kanalen ska ta emot data använder du operatorn <- före kanalen, som i följande exempel:

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

En annan åtgärd som du kan använda i en kanal är att stänga den. Om du vill stänga en kanal använder du den inbyggda close() funktionen:

close(ch)

När du stänger en kanal säger du att data inte kommer att skickas i kanalen igen. Om du försöker skicka data till en stängd kanal får programmet panik. Och om du försöker ta emot data från en stängd kanal kan du läsa alla data som skickas. Varje efterföljande "läsning" returnerar sedan ett nollvärde.

Vi går tillbaka till det program som vi skapade tidigare och använder kanaler för att ta bort vilolägesfunktionen. Först ska vi skapa en strängkanal i main funktionen, så här:

ch := make(chan string)

Och vi tar bort vilolägeslinjen time.Sleep(3 * time.Second).

Nu kan vi använda kanaler för att kommunicera mellan goroutines. I stället för att skriva ut resultatet i checkAPI funktionen ska vi omstrukturera koden och skicka meddelandet via kanalen. Om du vill använda kanalen från den funktionen måste du lägga till kanalen som parameter. Funktionen checkAPI bör se ut så här:

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

Observera att vi måste använda fmt.Sprintf funktionen eftersom vi inte vill skriva ut någon text, bara skicka formaterad text över kanalen. Observera också att vi använder operatorn <- efter kanalvariabeln för att skicka data.

Nu måste du ändra main funktionen för att skicka kanalvariabeln och ta emot data för att skriva ut den, så här:

ch := make(chan string)

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

fmt.Print(<-ch)

Observera hur vi använder operatorn <- innan kanalen säger att vi vill läsa data från kanalen.

När du kör programmet igen ser du utdata som den här:

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

Done! It took 0.007401217 seconds!

Det fungerar i alla fall utan anrop till en vilofunktion, eller hur? Men den gör fortfarande inte vad vi vill. Vi ser bara utdata från en av goroutines, och vi skapade fem. Nu ska vi se varför det här programmet fungerar på det här sättet i nästa avsnitt.

Obuffertade kanaler

När du skapar en kanal med hjälp make() av funktionen skapar du en obufferderad kanal, vilket är standardbeteendet. Obuffertade kanaler blockerar sändningsåtgärden tills det finns någon som är redo att ta emot data. Som vi sa tidigare blockerar sändning och mottagning åtgärder. Den här blockeringsåtgärden är också anledningen till att programmet från föregående avsnitt stoppades så snart det första meddelandet togs emot.

Vi kan börja med att säga att fmt.Print(<-ch) blockerar programmet eftersom det läser från en kanal och väntar på att vissa data ska tas emot. Så snart det har något fortsätter det med nästa rad och programmet avslutas.

Vad hände med resten av goroutines? De springer fortfarande, men ingen lyssnar längre. Och eftersom programmet avslutades tidigt kunde vissa goroutiner inte skicka data. För att bevisa den här punkten ska vi lägga till ytterligare en fmt.Print(<-ch), så här:

ch := make(chan string)

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

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

När du kör programmet igen ser du utdata som den här:

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

Observera att du nu ser utdata för två API:er. Om du fortsätter att lägga till fler fmt.Print(<-ch) rader läser du alla data som skickas till kanalen. Men vad händer om du försöker läsa mer data och ingen skickar data längre? Ett exempel är ungefär så här:

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)

När du kör programmet igen ser du utdata som den här:

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!

Det fungerar, men programmet slutförs inte. Den sista utskriftsraden blockerar den eftersom den förväntar sig att ta emot data. Du måste stänga programmet med ett kommando som Ctrl+C.

Det föregående exemplet bevisar bara att läsning av data och mottagande av data blockerar åtgärder. För att åtgärda det här problemet kan du ändra koden till en for loop och bara ta emot de data som du är säker på att du skickar, som i det här exemplet:

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

Här är den slutliga versionen av programmet om något gick fel med din version:

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

När du kör programmet igen ser du utdata som den här:

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!

Programmet gör vad det ska göra. Du använder inte längre en vilofunktion, du använder kanaler. Observera också att det nu tar cirka 600 ms att slutföras i stället för nästan 2 sekunder, när vi inte använde samtidighet.

Slutligen kan vi säga att obuffertade kanaler synkroniserar sändnings- och mottagningsåtgärderna. Även om du använder samtidighet är kommunikationen synkron.