Модуль: Concurrency · Уровень: Senior
TL;DR#
select ждёт готовности одного из нескольких канальных кейсов; при нескольких готовых выбирает псевдослучайно. default делает select неблокирующим. nil-каналы исключают кейс из выбора — основа для динамического включения/выключения и таймаутов/отмены через time.After/ctx.Done().
Теория#
Семантика#
- Если готов один кейс — он выполняется.
- Если готовы несколько — выбирается случайный (равномерно, через
fastrandn), чтобы избежать голодания. - Если ни один не готов и есть
default— выполняетсяdefault(неблокирующий select). - Если ни один не готов и
defaultнет — горутина блокируется, пока какой-то кейс не станет готов. - Пустой
select{}блокирует навсегда.
select {
case v := <-in:
use(v)
case out <- x:
// отправили
case <-time.After(time.Second):
// таймаут
default:
// ничего не готово прямо сейчас
}Под капотом (runtime.selectgo)#
- Кейсы перемешиваются в случайном
pollorder(для честного выбора). - Каналы блокируются в фиксированном
lockorderпо адресам — чтобы исключить дедлок при множественной блокировке. - Проход 1: ищем уже готовый кейс (есть данные/место/закрыт). Нашли — выполняем.
- Если есть
defaultи готовых нет — выполняемdefault. - Иначе: регистрируем
sudogво всех каналах (вsendq/recvq), паркуемся. - При пробуждении одним из каналов — снимаем регистрацию с остальных, выполняем выигравший кейс.
То есть в худшем случае select дороже одиночной операции: O(числа кейсов) на регистрацию/дерегистрацию.
nil-каналы в select#
Кейс с nil-каналом никогда не готов и не блокирует select целиком — он просто игнорируется. Это идиома «выключить» ветку:
var in <-chan int = source
var out chan<- int // nil пока нет данных
var pending int
for {
select {
case v, ok := <-in:
if !ok { in = nil; continue } // источник иссяк → выключаем чтение
pending = v; out = realOut // включаем запись
in = nil // выключаем чтение пока не отдадим
case out <- pending:
out = nil; in = source // отдали → снова читаем
}
}Таймауты#
select {
case res := <-work:
return res
case <-time.After(timeout): // ВНИМАНИЕ: таймер не освобождается до срабатывания
return ErrTimeout
}time.After создаёт таймер, который не собирается, пока не сработает. В цикле это утечка таймеров — используйте time.NewTimer + Stop() или (с Go 1.23 GC таймеров улучшен, но в цикле всё равно предпочтителен явный таймер) context.WithTimeout.
t := time.NewTimer(timeout)
defer t.Stop()
select {
case res := <-work:
return res
case <-t.C:
return ErrTimeout
}Отмена#
select {
case res := <-work:
return res, nil
case <-ctx.Done():
return zero, ctx.Err() // Canceled или DeadlineExceeded
}Каждая блокирующая канальная операция в долгоживущей горутине должна иметь ветку отмены, иначе — утечка.
Подводные камни / gotchas#
time.Afterв цикле = утечка таймеров и лишние аллокации. В горячем цикле —NewTimer/Resetилиctx.defaultв цикле опроса = busy-loop, сжигающий CPU. Не делайте polling-циклfor { select { default: } }без паузы.- Псевдослучайный выбор означает: нельзя полагаться на приоритет кейсов. Для приоритета нужен вложенный select (сначала high-priority с default, потом общий).
- Отправка в select тоже подчиняется правилам:
case ch <- vготов только если есть получатель/место; иначе блокируется. - Закрытый канал всегда «готов» на чтение — кейс
<-closedChбудет постоянно выигрывать, превращая select в busy-loop. После обнаружения закрытия обнуляйте канал (ch = nil). - Один и тот же канал в нескольких кейсах допустим, но усложняет рассуждения; выбор честный между всеми.
Приоритетный select#
// сначала пытаемся high, потом обычный выбор
select {
case v := <-high:
handle(v)
default:
select {
case v := <-high:
handle(v)
case v := <-low:
handle(v)
case <-ctx.Done():
return
}
}Вопросы на собеседовании#
В: Как select выбирает кейс, если готовы несколько? О: Псевдослучайно и равномерно (рантайм перемешивает порядок опроса). Это сделано намеренно, чтобы избежать систематического голодания одного из каналов.
В: Как сделать select неблокирующим?
О: Добавить default. Если ни один канал не готов мгновенно, выполнится default. Полезно для «попробовать без ожидания», но в цикле без паузы создаёт busy-loop.
В: Зачем нужны nil-каналы в select? О: Кейс с nil-каналом никогда не готов и не мешает остальным — это способ динамически включать/выключать ветки (например, переключаться между чтением источника и записью в приёмник), не меняя структуру select.
В: Чем опасен time.After в цикле?
О: Каждый вызов создаёт новый таймер, который держится до срабатывания, даже если кейс не выбран. В цикле это накапливает таймеры и память. Решение — time.NewTimer/Reset со Stop, либо context.WithTimeout.
В: Как реализовать таймаут и отмену операции?
О: Добавить в select ветки <-t.C (явный таймер) или <-ctx.Done(). Возвращать ctx.Err() для различения Canceled/DeadlineExceeded. Каждая блокирующая операция в долгоживущей горутине должна иметь такую ветку.
В: Что делает пустой select{}?
О: Блокирует горутину навсегда без потребления CPU (вечная парковка). Иногда используют в main, чтобы не выходить, пока живут фоновые горутины (хотя лучше явная синхронизация).
В: Как реализовать приоритет между каналами?
О: Вложенный select: сначала отдельный select по высокоприоритетному каналу с default; если он не готов — общий select по всем. Прямого приоритета в одном select нет из-за случайного выбора.
В: Почему select может превратиться в busy-loop?
О: Если в одном из кейсов закрытый канал (всегда готов на чтение) либо есть default в тесном цикле без паузы. Обнуляйте закрытые каналы (ch=nil) и не злоупотребляйте default.
В: Дороже ли select обычной операции с каналом?
О: Да, в случае блокировки: selectgo регистрирует горутину во всех каналах и снимает регистрацию при пробуждении — O(числа кейсов), плюс упорядоченная блокировка каналов во избежание дедлоков.
На что копают на senior+#
- Двухфазный протокол
selectgo: pollorder vs lockorder и почему блокировка по адресам предотвращает дедлок. - Семантика отправки в select и взаимодействие с
sendq/recvq. - Управление таймерами: устройство таймерного колеса/heap на P, изменения GC таймеров (1.23) и почему явный Stop всё ещё важен.
- Идиомы выключения кейсов через nil и проектирование state-machine на одном select.
- Голодание и честность: как случайный выбор влияет на латентность под перекосом нагрузки.