select
Представьте официанта, который стоит между двумя окнами раздачи: в одно подают горячее, в другое — напитки. Если он ждёт только у окна с горячим, готовые напитки так и стоят, пока не подадут горячее. Удобнее стоять между окнами и забирать то, что появилось первым.
С каналами то же самое. Приём из канала ждёт, пока в канале не появится значение. Когда значение может прийти по одному из двух каналов, ждать один из них нельзя: <-nums будет ждать вечно, если значение пришло в words.
Ожидание нескольких каналов
Оператор select ждёт сразу несколько операций с каналами и выполняет ту, что готова:
package main
import "fmt"
func main() {
nums := make(chan int)
words := make(chan string, 1)
words <- "go"
select {
case n := <-nums:
fmt.Println("number", n)
case w := <-words:
fmt.Println("word", w) // word go
}
}Давайте разберём пример по шагам:
nums := make(chan int)создаёт канал, в который никто не отправляет.words := make(chan string, 1)создаёт канал с буфером, иwords <- "go"кладёт в буфер"go".selectсмотрит на оба окна: приём изnumsне готов, приём изwordsготов.selectвыполняет второйcase, и программа печатаетword go.
Ветки select
select записывается как switch, но каждый case в нём — операция с каналом. Таких операций две.
Приём
Ветка case w := <-words: выполняется, когда из канала можно принять значение. Переменная, объявленная в приёме, доступна только внутри своего case, как w в примере. Принятое значение можно и не сохранять: case <-done:.
Отправка
Ветка может и отправлять: case ch <- v: выполняется, когда канал готов принять значение.
Как выбирается case
select подчиняется трём правилам:
- если ни одна операция не готова,
selectждёт, пока какая-нибудь не станет готова; selectвыполняет только одну операцию: остальныеcaseон пропускает и значений из их каналов не принимает;- если готовы несколько операций,
selectвыбирает одну из них случайно.
Здесь аналогия с официантом заканчивается: человек подошёл бы к ближнему окну, а select бросает жребий.
Порядок case в коде на выбор не влияет, в отличие от switch, который проверяет ветки сверху вниз. Поэтому программа с несколькими готовыми каналами может печатать разное при разных запусках. Этот вопрос часто задают на собеседовании: какой case выполнится, если готовы оба? Ответ — любой, выбор случайный.
Ветка default
Ветка default выполняется, когда ни одна операция не готова, и тогда select не ждёт:
ch := make(chan int, 1)
select {
case v := <-ch:
fmt.Println(v)
default:
fmt.Println("empty") // empty
}Разберём, что здесь происходит:
- канал пуст, поэтому выполняется
default: официант заглянул в окно, увидел пустую полку и пошёл заниматься другими делами; - если перед
selectотправить вchзначение, выполнится первыйcaseи напечатает его.
Так горутина проверяет канал, не останавливаясь: приём из пустого канала без select ждал бы, пока кто-нибудь не отправит значение.
Частая ошибка
select стоит внутри цикла for, и цикл пытаются закончить через break, когда закроется канал done:
for {
select {
case v := <-nums:
fmt.Println(v)
case <-done:
break // ✗
}
}break не выходит из цикла:
breakвнутриselectвыходит только изselect, как изswitch.- Цикл
forпродолжается. - Закрытый канал
doneготов к приёму всегда, поэтому цикл крутится вечно.
Исправить можно двумя способами:
- вынести цикл в отдельную функцию и заменить
breakнаreturn; - поставить перед
forметку и написатьbreakс её именем.
Главное
selectждёт несколько операций с каналами и выполняет одну готовую.- Если готовы несколько, выбор случайный; порядок
caseв коде не важен. defaultвыполняется, когда ничего не готово, и тогдаselectне ждёт.breakвнутриselectвыходит только изselect, а не из цикла вокруг него.
Подпишись, чтобы пройти курс
Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.