Używanie kanałów jako mechanizmu komunikacji
Kanał w języku Go to mechanizm komunikacji między goroutinami. Pamiętaj, że podejście języka Go do współbieżności jest następujące: "Nie przekazuj przez udostępnianie pamięci; zamiast tego udostępniaj pamięć, komunikując się". Jeśli musisz wysłać wartość z jednej goroutiny do innej, należy użyć kanałów. Zobaczmy, jak działają i jak można zacząć używać ich do pisania współbieżnych programów języka Go.
Składnia kanału
Ponieważ kanał jest mechanizmem komunikacyjnym, który wysyła i odbiera dane, ma również typ. Oznacza to, że można wysyłać dane tylko dla rodzaju obsługiwanego przez kanał. Słowo kluczowe chan
jest używane jako typ danych dla kanału, ale należy również określić typ danych, który będzie przekazywany przez kanał, jak int
typ.
Za każdym razem, gdy zadeklarujesz kanał lub chcesz określić kanał jako parametr w funkcji, musisz użyć polecenia chan <type>
, takiego jak chan int
. Aby utworzyć kanał, należy użyć wbudowanej make()
funkcji:
ch := make(chan int)
Kanał może wykonywać dwie operacje: wysyłać dane i odbierać dane. Aby określić typ operacji, którą ma kanał, należy użyć operatora <-
kanału . Ponadto wysyłanie danych i odbieranie danych w kanałach blokuje operacje. Zobaczysz za chwilę, dlaczego.
Jeśli chcesz powiedzieć, że kanał wysyła tylko dane, użyj <-
operatora po kanale. Jeśli chcesz, aby kanał odbierał dane, użyj <-
operatora przed kanałem, jak w poniższych przykładach:
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
Inną operacją, której można użyć w kanale, jest jej zamknięcie. Aby zamknąć kanał, użyj wbudowanej close()
funkcji:
close(ch)
Po zamknięciu kanału mówisz, że dane nie zostaną ponownie wysłane w tym kanale. Jeśli spróbujesz wysłać dane do zamkniętego kanału, program będzie panikować. A jeśli spróbujesz odebrać dane z zamkniętego kanału, będziesz w stanie odczytać wszystkie wysłane dane. Każda kolejna wartość "odczyt" zwróci wartość zero.
Wróćmy do utworzonego wcześniej programu i użyjemy kanałów, aby usunąć funkcjonalność uśpienia. Najpierw utwórzmy kanał ciągów w main
funkcji w następujący sposób:
ch := make(chan string)
Usuńmy linię time.Sleep(3 * time.Second)
usypiania .
Teraz możemy użyć kanałów do komunikowania się między goroutinami. Zamiast drukować wynik w checkAPI
funkcji, refaktoryzujemy nasz kod i wysyłamy ten komunikat za pośrednictwem kanału. Aby użyć kanału z tej funkcji, należy dodać kanał jako parametr. Funkcja checkAPI
powinna wyglądać następująco:
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)
}
Zwróć uwagę, że musimy użyć fmt.Sprintf
funkcji, ponieważ nie chcemy drukować żadnego tekstu, wystarczy wysłać sformatowany tekst w kanale. Zwróć również uwagę, że używamy <-
operatora po zmiennej kanału do wysyłania danych.
Teraz musisz zmienić funkcję, main
aby wysłać zmienną kanału i odebrać dane, aby je wydrukować, w następujący sposób:
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
fmt.Print(<-ch)
Zwróć uwagę, <-
że używamy operatora , zanim kanał mówi, że chcemy odczytywać dane z kanału.
Po ponownym uruchomieniu programu zobaczysz dane wyjściowe podobne do następujących:
ERROR: https://api.somewhereintheinternet.com/ is down!
Done! It took 0.007401217 seconds!
Przynajmniej działa bez wywołania funkcji uśpienia, prawda? Ale to nadal nie robi tego, czego chcemy. Widzimy dane wyjściowe tylko z jednego z goroutines i utworzyliśmy pięć. Zobaczmy, dlaczego ten program działa w ten sposób w następnej sekcji.
Niebuforowane kanały
Podczas tworzenia kanału make()
przy użyciu funkcji należy utworzyć niebuforowany kanał, który jest zachowaniem domyślnym. Niebuforowane kanały blokują operację wysyłania, dopóki ktoś nie będzie gotowy do odbierania danych. Jak powiedzieliśmy wcześniej, wysyłanie i odbieranie blokuje operacje. Ta operacja blokowania jest również przyczyną zatrzymania programu z poprzedniej sekcji zaraz po otrzymaniu pierwszego komunikatu.
Możemy zacząć od stwierdzenia, że fmt.Print(<-ch)
blokuje program, ponieważ odczytuje go z kanału i czeka na nadejście niektórych danych. Gdy tylko coś ma, będzie on kontynuowany z następnym wierszem, a program zostanie zakończony.
Co się stało z resztą gorynie? Nadal działają, ale nikt już nie słucha. A ponieważ program zakończył się wcześnie, niektóre goroutine nie mogły wysyłać danych. Aby udowodnić ten punkt, dodajmy kolejny fmt.Print(<-ch)
element w następujący sposób:
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
fmt.Print(<-ch)
fmt.Print(<-ch)
Po ponownym uruchomieniu programu zobaczysz dane wyjściowe podobne do następujących:
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
Done! It took 0.263611711 seconds!
Zwróć uwagę, że teraz zobaczysz dane wyjściowe dla dwóch interfejsów API. W przypadku kontynuowania dodawania kolejnych fmt.Print(<-ch)
wierszy skończy się odczytywanie wszystkich danych wysyłanych do kanału. Ale co się stanie, jeśli spróbujesz odczytać więcej danych i nikt już nie wysyła danych? Przykład wygląda następująco:
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)
Po ponownym uruchomieniu programu zobaczysz dane wyjściowe podobne do następujących:
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!
To działa, ale program nie kończy się. Ostatni wiersz wydruku blokuje go, ponieważ oczekuje się odbierania danych. Musisz zamknąć program za pomocą polecenia takiego jak Ctrl+C
.
W poprzednim przykładzie tylko okazuje się, że odczytywanie danych i odbieranie danych blokuje operacje. Aby rozwiązać ten problem, możesz zmienić kod na pętlę for
i odebrać tylko dane, które na pewno wysyłasz, podobnie jak w tym przykładzie:
for i := 0; i < len(apis); i++ {
fmt.Print(<-ch)
}
Oto ostateczna wersja programu, jeśli wystąpił problem z twoją wersją:
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)
}
Po ponownym uruchomieniu programu zobaczysz dane wyjściowe podobne do następujących:
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!
Program robi to, co ma zrobić. Nie używasz już funkcji uśpienia, używasz kanałów. Zauważ również, że teraz trwa około 600 ms, aby zakończyć zamiast prawie 2 sekund, gdy nie używaliśmy współbieżności.
Na koniec możemy powiedzieć, że niebuforowane kanały są synchronizowane z operacjami wysyłania i odbierania. Mimo że używasz współbieżności, komunikacja jest synchroniczna.