select ждёт, пока будет готов хотя бы один case, и среди готовых выбирает случайный. default — попытка без ожидания. Таймаут делают через timer или context, не через бесконечный sleep.
Конкурентность Go golang собеседование
Пока у горутины один канал, достаточно receive или send. Как только нужно «либо значение, либо отмена, либо дедлайн» — появляется select. Это ожидание нескольких событий, не switch по значению.
На бэкенде select стоит в воркерах, в прокси, в клиентах с deadline. Связка чтения из канала и ctx.Done() — нормальный ответ на «как перестать ждать, если клиент ушёл».
Аналогия: сидите у двух дверей. Кто первый постучит — того и откроете. Постучали обе сразу — выберете одну наугад, не «левую, потому что она выше в коде».
Таймаут и отмена
Готовых case может быть несколько. Select не берёт верхний в исходнике как приоритет. Среди готовых выбор псевдослучайный. Отдельный case теоретически может голодать, если другой всегда готов. На практике для отмены и данных это редко проблема, если отмена тоже готова.
Ветку по времени делают через time.After или, лучше, через context.WithTimeout. Context отменяет всё дерево вызовов одним Done. Таймер в каждом select — локальнее и проще забыть.
select {
case v := <-ch:
process(v)
case <-ctx.Done():
return ctx.Err()
case <-time.After(200 * time.Millisecond):
return errTimeout
}
Отмена и дедлайн в одном select. time.After в тесном цикле копит таймеры.
Пример. Клиент закрыл вкладку. Хендлер без case <-ctx.Done() всё ещё ждёт ответ соседнего сервиса. Слот воркера занят, пока не истечёт чужой таймаут.
time.After каждый раз создаёт новый timer в куче. В горячем цикле не сработавшие таймеры живут до истечения — утечка и лишняя работа. Для повторных ожиданий берут time.NewTimer, проверяют Stop и делают Reset. Для дедлайна всего запроса предпочтителен context: его и так передают в HTTP и SQL.
default и пустой select
Ветка default выполняется сразу, если никто не готов. Это попытка send или receive без блокировки. Полезно проверить канал и идти дальше. В цикле for { select { default: ... } } без ожидания получается busy loop: горутина крутит процессор вхолостую. Пустой select {} блокирует навсегда. Так иногда «паркуют» main, но это не обработка сигналов.
Пример. Нужно «забрать из канала, если есть, иначе продолжить». Пишут select { case v := <-ch: ... default: }. Если default убрать, горутина заснёт до первого значения.
Отключить case можно, присвоив каналу nil: receive и send на nil в select просто никогда не готовы. Так выкидывают исчерпанный источник, не меняя число веток.
Типичная ошибка
Крутить select с default в for без паузы — занятый CPU. Вторая: time.After внутри тесного цикла. Третья — ждать, что верхний case всегда приоритетнее: среди готовых выбор случайный.
На собеседовании
Связка select + ctx.Done() — основной способ бросить чтение из канала. default — попытка без ожидания, не обязательная ветка. Таймаут лучше через context или Reset таймера, не через After в горячем цикле.