Czekanie na kilka kanałów
select wygląda jak switch, ale każdy przypadek to wysłanie do kanału lub odbiór z niego. Blokuje się, aż jeden przypadek będzie mógł się wykonać, i wtedy go uruchamia.
Przy tych opóźnieniach pierwszy select dostaje szybki wynik, a drugi wolny. Żaden odbiór nie musi czekać na drugi kanał. Zwykłe <-slow, a po nim <-fast obsłużyłyby je w stałej kolejności, bez względu na to, który przyszedł pierwszy.
Jak select jest wykonywany:
- Wszystkie wyrażenia kanałów i wartości do wysłania są obliczane raz, w kolejności z kodu, gdy
selectsię zaczyna. - Jeśli gotowy jest jeden lub więcej przypadków, jeden z nich jest wybierany losowo.
- Jeśli żaden nie jest gotowy, a istnieje
default, wykonuje siędefault. - W przeciwnym razie goroutine blokuje się, dopóki któryś przypadek nie stanie się gotowy.
Pusty select {} blokuje na zawsze. Czasem widać go na końcu main w programach, których właściwa praca odbywa się w innych goroutine.
Losowy wybór spośród gotowych przypadków
Gdy kilka przypadków jest gotowych jednocześnie, select nie preferuje pierwszego z listy. Ten program wypełnia dwa buforowane kanały, a potem wykonuje select 1000 razy:
Podział między countA a countB zmienia się przy każdym uruchomieniu i wychodzi blisko 500 dla każdego. Losowy wybór jest zamierzony: zapobiega temu, by zajęty kanał zagłodził pozostałe. Jeśli potrzebujesz priorytetu, zobacz wzorzec niżej.
Operacje nieblokujące z default
Z przypadkiem default instrukcja select nigdy się nie blokuje. To zamienia wysłanie lub odbiór w operację typu „spróbuj”:
Porzucanie pracy, gdy bufor jest pełny, to sposób na zrzucanie nadmiaru obciążenia albo wysyłanie metryk bez wstrzymywania wywołującego.
Nie wstawiaj default do select w pętli for tylko po to, żeby w kółko „sprawdzać” kanały. Gdy nic nie jest gotowe, pętla kręci się na 100% CPU. Zamiast tego blokuj i dodaj przypadek z timeoutem, jeśli musisz się okresowo wybudzać.
Timeouty
time.After(d) zwraca kanał, który odbiera wartość raz, po czasie d. Postaw go do wyścigu z właściwą pracą:
Pierwsze wywołanie zwraca "data". Drugie zwraca błąd timeoutu po 50 ms, na długo zanim worker by skończył. Kanał wyniku ma celowo bufor o rozmiarze 1. Gdy wygrywa timeout, nikt nigdy nie odbiera z result; przy kanale niebuforowanym goroutine workera zablokowałaby się na wysyłaniu na zawsze i wyciekłaby.
W pętli time.After tworzy nowy timer w każdej iteracji, co jest dokładnie tym, czego chcesz dla timeoutu bezczynności na wiadomość („brak wiadomości przez 1 sekundę”). Dla łącznego deadline'u obejmującego wiele operacji utwórz jeden timer lub kontekst przed pętlą. Od Go 1.23 timery, do których nie ma już odwołań, są zbierane przez garbage collector, nawet jeśli jeszcze nie odpaliły, więc time.After w pętli nie trzyma już pamięci do odpalenia każdego timera, jak w starszych wersjach (wymaga to go 1.23 lub nowszego w go.mod).
Pętle for-select i kanały quit
Goroutine, która działa, dopóki nie dostanie polecenia zatrzymania, to pętla for wokół select z jednym przypadkiem dla pracy i jednym dla zatrzymania:
Idiomem jest zamknięcie quit zamiast wysyłania do niego: zamknięcie widzi każdy odbiorca, teraz i później, więc jedno close zatrzymuje dowolną liczbę workerów. Kanał done pozwala main poczekać, aż worker naprawdę się zakończy.
W prawdziwym kodzie kanałem quit jest zwykle context.Context: case <-ctx.Done():. Działa tak samo (Done() zwraca kanał, który jest zamykany przy anulowaniu), a do tego przenosi deadline'y i powód zatrzymania. Opisuje to strona o kontekście.
break wewnątrz select
break w przypadku select wychodzi z select, a nie z otaczającej pętli for. To częste źródło pętli, które nigdy się nie kończą. Użyj return albo dodaj etykietę do pętli:
Priorytet między kanałami
Ponieważ select wybiera losowo, w jednej instrukcji nie da się ustalić kolejności przypadków. Aby jeden kanał wygrywał zawsze, gdy ma coś do odebrania, sprawdź go najpierw osobno:
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
select {
case <-ctx.Done():
return ctx.Err()
case job := <-jobs:
handle(job)
}
}
Pierwszy select od razu kończy funkcję, jeśli anulowanie już nastąpiło. Bez niego stały strumień zadań mógłby jeszcze przez jakiś czas wygrywać losowanie po anulowaniu ctx.
Kanały nil wyłączają przypadek
Wysłanie lub odbiór na kanale nil nigdy nie jest gotowe, więc przypadek z kanałem nil jest w praktyce wyłączony. Ustawienie zamkniętego wejścia na nil to sposób, by przestać na nie czekać w select i dalej obsługiwać pozostałe; strona o kanałach pokazuje zbudowaną w ten sposób pętlę scalającą. Ta sama sztuczka włącza i wyłącza timeout: trzymaj var timeout <-chan time.Time jako nil, dopóki go nie potrzebujesz, a potem przypisz time.After(d).
Typowe błędy
- Oczekiwanie kolejności z kodu. Pierwszy przypadek na liście nie jest preferowany.
- Pętla aktywnego oczekiwania z
default.for { select { ... default: } }bez pracy do wykonania pali cały rdzeń CPU. - Wyciek przegranej goroutine. Gdy wygrywa timeout, goroutine, która miała wysłać wynik, nadal musi móc się zakończyć. Daj jej kanałowi bufor o rozmiarze 1.
break, który wychodzi tylko zselect. Użyj etykiety lubreturn.- Jedno
time.Afterna iterację pętli użyte jako łączny deadline. Restartuje się w każdej iteracji; utwórz deadline raz, poza pętlą.
Najczęściej zadawane pytania
Co robi select w Go?
select czeka, aż jedna z jego operacji na kanałach (wysłanie lub odbiór) będzie mogła się wykonać, i wtedy uruchamia ten przypadek. Jeśli kilka jest gotowych jednocześnie, wybiera jeden losowo. Bez przypadku default blokuje się, dopóki któryś przypadek nie będzie gotowy; z default nigdy się nie blokuje.
Jak dodać timeout do odbioru z kanału w Go?
Umieść odbiór i timer w jednym select: select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }. Wygrywa to, co nastąpi pierwsze. W pętli albo gdy wywołujący ma już deadline, użyj zamiast tego context.Context z context.WithTimeout i czekaj w select na ctx.Done().
Czy select w Go wybiera przypadki po kolei?
Nie. Gdy gotowy jest więcej niż jeden przypadek, Go wybiera losowo z równym prawdopodobieństwem, więc żaden przypadek nie może zagłodzić pozostałych. Jeśli potrzebujesz priorytetu, najpierw sprawdź kanał o wysokim priorytecie we własnym select z default, a dopiero potem przejdź do select po wszystkich kanałach.
Dlaczego break nie wychodzi z mojej pętli for-select?
Wewnątrz select instrukcja break wychodzi tylko z select, a nie z otaczającej pętli for. Użyj return albo dodaj etykietę do pętli (loop: for { select { case <-done: break loop } }).