Как записать в канал в Go без блокировки, если канал полный
Что проверяют
Знаете ли вы select с веткой default и понимаете ли, что делать с данными, которые не влезли. Сама конструкция занимает пару строк, интересен разговор вокруг: терять значение, ждать с таймаутом или вытеснять старое. Следом часто спрашивают, что будет при записи в закрытый канал.
Как отвечать
Базовый ответ: select с веткой default. Если отправка case ch <- v не может выполниться прямо сейчас, сразу срабатывает default, и горутина не блокируется. Затем варианты поведения:
- отбросить значение: в
defaultувеличить счётчик потерь и идти дальше. Так делают для метрик, логов и событий, где свежесть важнее полноты; - подождать ограниченно: вместо
defaultветкаcase <-ctx.Done()илиcase <-time.After(d). Отправка блокируется, но не дольше таймаута. С Go 1.23 неостановленные таймеры собирает сборщик мусора, поэтомуtime.Afterв цикле больше не копит память, ноcontextвсё равно удобнее для отмены; - вытеснить старое: при полном канале прочитать одно значение и записать новое. Корректно это работает только при одном отправителе, иначе между чтением и записью место займёт другая горутина;
- нарастить буфер:
make(chan T, n)сглаживает всплески, но не решает проблему медленного потребителя, а лишь откладывает её.
Уточните, что для небуферизованного канала отправка через select с default пройдёт, только если получатель уже ждёт в этот момент.
Где спотыкаются
Проверяют len(ch) < cap(ch) перед записью. Между проверкой и отправкой другая горутина может заполнить канал, и запись заблокируется. Атомарная попытка отправки одна: select.
Считают, что select с default защищает от закрытого канала. Отправка в закрытый канал вызывает панику и внутри select, поэтому закрывать канал должен тот, кто в него пишет, и только когда других писателей не осталось.
Молча теряют данные в default. Без счётчика или лога вы узнаете о переполнении только по пропавшим событиям в проде, а ведь это ещё и сигнал, что потребитель не справляется.