Ожидание нескольких каналов
select похож на switch, но каждая его ветка это отправка в канал или получение из канала. Он блокируется, пока одна ветка не сможет выполниться, и выполняет её.
С такими задержками первый select получает быстрый результат, а второй медленный. Ни одному получению не нужно ждать другого канала. Обычные <-slow и затем <-fast обработали бы их в фиксированном порядке, независимо от того, что пришло первым.
Как вычисляется select:
- Все выражения каналов и отправляемые значения вычисляются один раз, в порядке исходного кода, когда
selectначинается. - Если готова одна или несколько веток, одна из них выбирается случайно.
- Если ни одна не готова и есть
default, выполняетсяdefault. - Иначе горутина блокируется, пока какая-то ветка не станет готовой.
Пустой select {} блокируется навсегда. Иногда его можно увидеть в конце main в программах, чья настоящая работа идёт в других горутинах.
Случайный выбор среди готовых веток
Когда одновременно готовы несколько веток, select не отдаёт предпочтение той, что указана первой. Эта программа заполняет два буферизованных канала, а затем делает select 1000 раз:
Распределение между countA и countB меняется при каждом запуске и держится около 500 на каждый. Случайный выбор сделан намеренно: он не даёт загруженному каналу заморить голодом остальные. Если нужен приоритет, смотрите шаблон ниже.
Неблокирующие операции через default
С веткой default оператор select никогда не блокируется. Это превращает отправку или получение в операцию «попробовать»:
Отбрасывать работу, когда буфер полон, это способ сбросить нагрузку или отправлять метрики, никогда не останавливая вызывающий код.
Не ставьте default в select внутри цикла for только ради того, чтобы снова и снова «проверять» каналы. Когда ничего не готово, цикл крутится на 100% CPU. Лучше блокируйтесь, а если нужно периодически просыпаться, добавьте ветку с таймаутом.
Таймауты
time.After(d) возвращает канал, который получает значение один раз через d. Устройте гонку между ним и настоящей работой:
Первый вызов возвращает "data". Второй возвращает ошибку таймаута через 50 мс, задолго до того, как воркер закончил бы. У канала результата буфер 1 намеренно. Когда побеждает таймаут, из result никто никогда не читает; с небуферизованным каналом горутина-воркер навсегда заблокировалась бы на отправке и утекла.
В цикле time.After на каждой итерации создаёт новый таймер, и для таймаута простоя на каждое сообщение («нет сообщений 1 секунду») это ровно то, что нужно. Для общего дедлайна на множество операций создайте один таймер или контекст до цикла. Начиная с Go 1.23 таймеры, на которые больше никто не ссылается, собираются сборщиком мусора, даже если ещё не сработали, поэтому time.After в цикле больше не держит память до срабатывания каждого таймера, как в старых версиях (для этого в go.mod нужно go 1.23 или новее).
Циклы for-select и каналы остановки
Горутина, которая работает, пока её не остановят, это цикл for вокруг select с одной веткой для работы и одной для остановки:
Идиома в том, чтобы закрывать quit, а не отправлять в него: закрытие видит каждый получатель, сейчас и позже, так что один close останавливает любое число воркеров. Канал done позволяет main дождаться, пока воркер действительно вернётся.
В реальном коде канал остановки обычно это context.Context: case <-ctx.Done():. Работает так же (Done() возвращает канал, который закрывается при отмене), но вдобавок несёт дедлайны и причину остановки. Подробности на странице о context.
break внутри select
break в ветке select выходит из select, а не из охватывающего for. Это частый источник циклов, которые никогда не заканчиваются. Используйте return или пометьте цикл:
Приоритет между каналами
Поскольку select выбирает случайно, ранжировать ветки внутри одной инструкции нельзя. Чтобы один канал побеждал всякий раз, когда в нём что-то есть, сначала проверьте его отдельно:
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
select {
case <-ctx.Done():
return ctx.Err()
case job := <-jobs:
handle(job)
}
}
Первый select сразу возвращается, если отмена уже произошла. Без него постоянный поток заданий мог бы ещё какое-то время выигрывать случайный выбор после отмены ctx.
nil-каналы отключают ветку
Отправка или получение на nil-канале никогда не готовы, так что ветка с nil-каналом фактически выключена. Присвоить закрытому входному каналу nil это способ перестать делать select по нему, продолжая работать с остальными; на странице о каналах показан построенный так цикл слияния. Тем же приёмом включают и выключают таймаут: держите var timeout <-chan time.Time равным nil, пока он не нужен, а затем присвойте time.After(d).
Частые ошибки
- Расчёт на порядок в коде. Первая по списку ветка не имеет преимущества.
- Холостой цикл с
default.for { select { ... default: } }, когда делать нечего, сжигает ядро процессора. - Утечка проигравшего. Когда побеждает таймаут, горутина, которая должна была отправить результат, всё равно должна иметь возможность завершиться. Дайте её каналу буфер 1.
break, который выходит только изselect. Используйте метку илиreturn.- Один
time.Afterна итерацию цикла в роли общего дедлайна. Он перезапускается на каждой итерации; создайте дедлайн один раз, вне цикла.
Часто задаваемые вопросы
Что делает select в Go?
select ждёт, пока одна из его операций с каналами (отправка или получение) сможет выполниться, и выполняет эту ветку. Если одновременно готовы несколько, он выбирает одну случайно. Без ветки default он блокируется, пока какая-то ветка не станет готовой; с default он не блокируется никогда.
Как добавить таймаут к получению из канала в Go?
Поместите получение и таймер в один select: select { case v := <-ch: use(v); case <-time.After(2 * time.Second): return errTimeout }. Побеждает то, что случится раньше. В цикле или когда у вызывающего кода уже есть дедлайн используйте context.Context с context.WithTimeout и делайте select по ctx.Done().
Выбирает ли select в Go ветки по порядку?
Нет. Когда готово больше одной ветки, Go выбирает равновероятно случайную, поэтому ни одна ветка не может заморить голодом остальные. Если нужен приоритет, сначала проверьте приоритетный канал в отдельном select с default, а затем переходите к select по всем каналам.
Почему break не выходит из моего цикла for-select?
Внутри select оператор break выходит только из инструкции select, а не из окружающего for. Используйте return или поставьте метку на цикл (loop: for { select { case <-done: break loop } }).