Wysyłanie i odbieranie
Kanał to typowany przewód między gorutynami. ch <- v wysyła, <-ch odbiera. Kanał tworzy się przez make:
Wartość zerowa typu kanałowego to nil, więc var ch chan string bez make daje kanał, który blokuje w nieskończoność. Zawsze twórz kanały przez make.
Kanały niebuforowane synchronizują
make(chan T) tworzy kanał niebuforowany. Wysłanie blokuje, dopóki odbiorca nie weźmie wartości, a odbiór blokuje, dopóki nadawca jej nie dostarczy. Obie gorutyny spotykają się w tym punkcie, dlatego kanał niebuforowany jest w równym stopniu narzędziem synchronizacji, co przewodem na dane. Kiedy poniżej <-done zwraca, wiadomo, że worker skończył wszystko, co było przed wysłaniem:
Odczyt result w main jest tu bezpieczny bez mutexa. Model pamięci Go gwarantuje, że wszystko, co worker zrobił przed wysłaniem, jest widoczne dla main po odpowiadającym mu odbiorze. chan struct{} to idiomatyczny typ dla czystego sygnału, bo struct{} nie zajmuje pamięci.
Kanały buforowane
make(chan T, n) daje kanałowi miejsce na n wartości. Wysyłanie działa bez odbiorcy, dopóki bufor się nie zapełni; odbieranie działa, dopóki bufor nie jest pusty. Wartości wychodzą w tej kolejności, w jakiej weszły.
Bufor oddziela nadawcę od odbiorcy, więc krótkie serie nie wstrzymują nadawcy. Nie naprawia jednak producenta, który jest stale szybszy od konsumenta; jedynie opóźnia moment, w którym nadawca się zablokuje. Rozmiar bufora dobieraj z konkretnego powodu (liczba nadawców, znany rozmiar partii), a nie po to, żeby zniknął deadlock.
len(ch) to migawka. Zanim na jej podstawie coś zrobisz, inna gorutyna mogła już ją zmienić, więc nie używaj jej do sprawdzania, czy wysłanie zablokuje. Do tego służy select z gałęzią default.
Close i range
close(ch) informuje odbiorców, że więcej wartości nie zostanie wysłanych. Po zamknięciu:
- wartości, które są już w buforze, nadal zostają dostarczone,
- potem każdy odbiór od razu zwraca wartość zerową,
v, ok := <-chzwracaok == false,for v := range chsię kończy.
Pierwszy odbiór po close nadal dostaje "last" z ok == true. Drugi dostaje wartość zerową "" i false.
Reguły, które kończą się panic:
- wysłanie do zamkniętego kanału wywołuje panic z komunikatem
send on closed channel, - zamknięcie już zamkniętego kanału wywołuje panic,
- zamknięcie kanału
nilwywołuje panic.
Dlatego zamyka tylko strona wysyłająca i tylko raz. Przy kilku nadawcach żaden z nich nie wie, kiedy pozostali skończyli; niech osobna gorutyna poczeka na wszystkich nadawców (przez sync.WaitGroup) i zamknie kanał, gdy Wait zwróci. Zamykanie jest potrzebne tylko wtedy, gdy odbiorca czeka na koniec. Niezamknięty kanał, do którego nikt się nie odwołuje, jest sprzątany przez garbage collector jak każda inna wartość.
Typy kierunkowe
Funkcja może zadeklarować, że na kanale tylko wysyła albo tylko odbiera. Kompilator odrzuci wtedy drugą operację.
| Typ | Znaczenie | Dozwolone |
|---|---|---|
chan T | dwukierunkowy | wysyłanie, odbiór, zamknięcie |
chan<- T | tylko do wysyłania | wysyłanie, zamknięcie |
<-chan T | tylko do odbioru | odbiór |
chan T konwertuje się niejawnie na każdy z typów ograniczonych, gdy przekazujesz go do funkcji. Dlatego produce powyżej może zwrócić <-chan int: wywołujący mogą po nim iterować przez range, ale nie mogą do niego wysyłać ani go zamknąć. Używaj typów kierunkowych w każdym parametrze funkcji, w którym mają sens. Dokumentują własność i zamieniają niewłaściwe użycie w błąd kompilacji.
Deadlock
Jeśli każda gorutyna jest zablokowana i nic nie może żadnej z nich obudzić, środowisko uruchomieniowe przerywa program:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/main.go:7 +0x38
exit status 2
Zrzut gorutyn pokazuje, która operacja utknęła (tu chan send, gdzie indziej chan receive, sync.WaitGroup.Wait, select). Typowe przyczyny:
- wysłanie do kanału niebuforowanego, gdy nie działa żaden odbiorca,
rangepo kanale, który nigdy nie zostaje zamknięty,- licznik
WaitGroup, który nigdy nie spada do zera, - dwie gorutyny, z których każda czeka na drugą.
Środowisko uruchomieniowe wykrywa tylko sytuację, w której utknęły wszystkie gorutyny. W serwerze z innymi żywymi gorutynami (listener HTTP, ticker) ten sam błąd niczego nie wywraca; zablokowana gorutyna po prostu wycieka.
Kanały nil
Wysyłanie i odbieranie na kanale nil blokuje w nieskończoność. Brzmi to bezużytecznie, ale to standardowa sztuczka w select: ustawienie zmiennej kanału na nil wyłącza jej gałąź. Ten kod łączy dwa kanały i przestaje nasłuchiwać każdego z nich, gdy zostanie zamknięty:
Bez przypisań nil zamknięty kanał jest zawsze gotowy i pętla kręciłaby się w kółko na wartościach zerowych.
Pipeline
Kanały składają się w pipeline'y: każdy etap to gorutyna, która odbiera z jednego kanału i wysyła do następnego, a zamyka swoje wyjście, gdy skończy się jej wejście.
Każdy etap działa współbieżnie, a kolejność wyników jest deterministyczna (1, 16, 81), bo każdy etap to jedna gorutyna zachowująca kolejność. Zamknięcia przechodzą kaskadowo: generate zamyka kanał, co kończy range w square, które zamyka swoje wyjście, i tak dalej aż do main.
Słaby punkt tego pipeline'u: gdyby main przestało czytać wcześniej, etapy zablokowałyby się na wysyłaniu na zawsze. Prawdziwe pipeline'y przyjmują context.Context albo kanał done i obok każdego wysłania robią na nim select.
Kanał czy mutex
Kanały służą do przekazywania własności danych i sygnalizowania zdarzeń. sync.Mutex jest prostszy do ochrony wspólnego stanu, który wiele gorutyn czyta i zmienia w miejscu, jak cache albo licznik. Struktura z mutexem w środku jest często czytelniejsza niż gorutyna, która posiada stan i obsługuje żądania przez kanały. Wybierz to, co daje krótszy kod i oczywistą własność.
Ściągawka
| Operacja | kanał nil | kanał otwarty | kanał zamknięty |
|---|---|---|---|
ch <- v | blokuje na zawsze | blokuje do odbioru lub wolnego miejsca w buforze | panic |
<-ch | blokuje na zawsze | blokuje, aż pojawi się wartość | wartości z bufora, potem wartość zerowa |
v, ok := <-ch | blokuje na zawsze | ok ma wartość true | ok ma wartość false po opróżnieniu |
close(ch) | panic | zamyka | panic |
len(ch), cap(ch) | 0, 0 | wartości w buforze, rozmiar bufora | pozostałe wartości, rozmiar bufora |
Najczęściej zadawane pytania
Czym różni się kanał buforowany od niebuforowanego w Go?
Kanał niebuforowany (make(chan int)) nie ma miejsca na wartości: wysłanie blokuje, dopóki inna gorutyna nie odbierze, więc każde wysłanie jest też przekazaniem z rąk do rąk i punktem synchronizacji. Kanał buforowany (make(chan int, 3)) mieści do 3 wartości; wysyłanie blokuje tylko przy pełnym buforze, a odbiór tylko przy pustym.
Co się dzieje przy odczycie z zamkniętego kanału w Go?
Odbiór z zamkniętego kanału nigdy nie blokuje. Najpierw zwraca wartości, które zostały jeszcze w buforze, a potem już zawsze wartość zerową typu elementu. Użyj v, ok := <-ch, żeby odróżnić te przypadki: ok ma wartość false, gdy kanał jest zamknięty i pusty. Pętla for v := range ch kończy się właśnie w tym momencie.
Kto powinien zamykać kanał w Go?
Nadawca, i tylko wtedy, gdy odbiorcy muszą wiedzieć, że więcej wartości nie będzie (na przykład żeby zakończyć pętlę range). Wysłanie do zamkniętego kanału wywołuje panic, tak samo jak dwukrotne zamknięcie, więc odbiorca zamykający kanał ściga się z nadawcami. Nie trzeba zamykać kanału, żeby go zwolnić; garbage collector i tak usuwa nieosiągalne kanały.
Co oznacza "fatal error: all goroutines are asleep"?
Każda gorutyna w programie jest zablokowana na operacji na kanale lub na blokadzie, której nic nigdy nie zakończy, więc środowisko uruchomieniowe zatrzymuje program. Najczęstsza przyczyna to wysyłanie do kanału niebuforowanego w main, gdy żadna inna gorutyna nie odbiera, albo range po kanale, który nigdy nie zostaje zamknięty.