Перед выбором ветви select какие выражения уже вычислены?
До выбора готовой ветви select Go один раз вычисляет выражения каналов, а для операций отправки — ещё и выражение отправляемого значения. Это происходит даже для той ветви, которая в итоге не будет выбрана, поэтому побочные эффекты и дорогие вычисления нельзя помещать в выражение отправки без учёта этого правила.
select предназначен для ожидания нескольких коммуникационных операций без ручного опроса каналов и активного ожидания. Чтобы определить набор потенциально готовых операций, рантайму нужны уже вычисленные каналы и значения отправки; поэтому вычисление этих выражений отделено от последующего выбора ветви.
Разработчик может ожидать, что выражение отправки выполнится только при успешном выборе соответствующего case. Это неверно: даже если канал не готов и срабатывает default, функция построения значения уже могла выполниться, изменить состояние, записать метрику или вызвать панику.
Такое поведение приводит к неожиданным затратам, побочным эффектам и ошибкам повторной обработки. Особенно опасны операции сериализации, обращение к внешним ресурсам и функции, которые должны вызываться только после подтверждения возможности отправки.
При входе в select выражения каналов и выражения отправляемых значений вычисляются ровно один раз в порядке, определённом спецификацией. Затем Go проверяет, какие коммуникации могут продолжиться, и выбирает одну из готовых ветвей; если готовых ветвей нет, используется default, если он присутствует.
Например:
Канал небуферизованный, а получателя нет, поэтому отправка не готова и выбирается default. Однако build всё равно вызывается до выбора ветви.
Практическое правило: выражения, необходимые только после успешного выбора, нужно выполнять внутри тела выбранной ветви. Если значение требуется именно для операции отправки, следует заранее учитывать возможное вычисление впустую и отсутствие гарантии, что отправка состоится.
Важно не смешивать вычисление выражений с синхронизацией. Сам факт, что выражение вычислено до выбора ветви, не означает, что другая горутина увидит его побочные эффекты или что операция отправки обязательно произойдёт. Нужные гарантии видимости возникают из конкретных синхронизирующих операций, а не из порядка вычисления выражений в select.
Сервис пытался неблокирующе отправлять события в канал мониторинга. В выражении отправки выполнялась дорогая сериализация события, а при переполнении потребитель не успевал принять данные и срабатывал default. В результате сериализация происходила даже для отброшенных событий, создавая лишнюю нагрузку на процессор и сборщик мусора.
Рассматривались варианты:
select — логика проста, но вычисление всё равно часто выполняется впустую;default — отправитель перестаёт терять события, но может заблокировать критический путь;Для некритичных метрик выбрали отдельную ограниченную очередь с фоновой горутиной и политикой отбрасывания уже готовых компактных событий. Это отделило бизнес-операцию от медленной отправки; при этом размер очереди и количество отброшенных событий стали наблюдаемыми метриками.
Вопрос: Вычисляется ли выражение отправки, если в select срабатывает default?
Ответ: Да. Правая часть операции отправки вычисляется при входе в select, до определения готовой ветви. Поэтому default предотвращает блокировку, но не отменяет затрат и побочных эффектов вычисления значения.
Вопрос: В каком порядке вычисляются выражения разных ветвей select?
Ответ: Выражения каналов и правые части отправок вычисляются один раз в порядке, заданном спецификацией, до выбора ветви. Нельзя считать, что Go сначала проверит готовность одного case, а затем вычислит выражения другого: побочные эффекты всех таких выражений могут произойти ещё до выбора.
Вопрос: Является ли вычисление выражений перед выбором select способом синхронизации горутин?
Ответ: Нет. Это лишь правило вычисления внутри одной горутины. Оно не создаёт гарантии о порядке наблюдения данных другой горутиной; для синхронизации нужны канал, мьютекс, WaitGroup, атомарные операции или другой подходящий примитив.