Каналы — главный примитив коммуникации в Go, и каждая грань у него острая
Go строит конкурентность вокруг одного лозунга — «не делите память, общайтесь через каналы». Channel — это не просто очередь: это типизированная труба, которая синхронизирует горутины и устанавливает между ними отношение happens-before. Передали значение через channel — и компилятор с рантаймом гарантируют, что получатель видит всё, что отправитель записал до отправки. Именно поэтому каналы заменяют мьютексы в идиоматичном Go-коде.
Но за удобством прячется набор острых граней, и каждая из них — стандартный вопрос на собеседовании. Отправка в закрытый channel не «молча отбрасывается» — она роняет программу панкой. Операция на nil channel не паникует — она блокируется навсегда. Закрытие из получателя или из нескольких отправителей паникует на двойном закрытии. select без default блокируется, пустой select{} блокируется вечно, а горутина, навсегда заблокированная на channel, который никто не удовлетворит, — это утечка, которую сборщик мусора не уберёт. Эта тема разбирает channel по слоям — от устройства рантайм-структуры hchan до дерева узлов context.
Карта темы
- Механика каналов — что такое channel под капотом — структура
hchan, рандеву небуферизированного канала, отношение happens-before. - Буферизированные каналы —
make(chan T, n), кольцевой буфер, когда именно блокируется отправка и приём. - nil-каналы — почему отправка и приём на
nilchannel виснут навсегда и как этим пользуются вselect. - Поведение закрытого канала — comma-ok,
rangeпо каналу, паника при отправке и при повторном закрытии. - Оператор select — равновероятный выбор готовой ветки,
defaultкак неблокирующий путь, пустойselect{}. - Утечки горутин — как заблокированная навсегда горутина тихо копится и почему GC её не уберёт.
- Отмена через context — как
context.Contextпереносит сигнал отмены и распространяет его по дереву. - Устройство context —
cancelCtx,timerCtx,valueCtx, ленивыйDone, цепочка значений.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
| Считать, что буферизированный channel никогда не блокирует отправителя | Отправка блокируется при заполнении буфера; ложная модель пропускной способности |
| Думать, что у небуферизированного канала буфер размера один | На деле размер ноль — это синхронное рандеву, а не «буфер на одного» |
Считать, что операция на nil channel паникует | Она блокируется навсегда; перепутанная с закрытым каналом — тихий дедлок |
| Думать, что отправка в закрытый channel молча отбрасывается | Паника send on closed channel — программа падает |
| Закрывать channel из получателя или из нескольких отправителей | Двойное закрытие и отправка в закрытый channel паникуют |
Выдумывать встроенную функцию closed(ch) | Такого предиката в Go нет; закрытие обнаруживают через comma-ok |
Считать, что ветки select приоритизируются по порядку | Выбор среди готовых веток равновероятно случайный |
Думать, что default заставляет select опрашивать каналы в цикле | default выполняется сразу, если ни одна ветка не готова — это один проход, а не busy-poll |
| Полагать, что GC уберёт горутину, заблокированную на channel навсегда | Заблокированная горутина достижима из рантайма — это утечка, не мусор |
Думать, что отмена context принудительно убивает горутины | Отмена лишь закрывает Done(); горутина обязана сама проверить сигнал |
| Считать, что отмена идёт от ребёнка к родителю | Отмена распространяется от родителя ко всем дочерним контекстам |
Забыть вызвать функцию cancel | Утекает timerCtx-таймер и узел остаётся в дереве до отмены родителя |
Значение для собеседований
Каналы — обязательная тема на любом Go-интервью, и спрашивают не синтаксис <-, а модель блокировки под ним. Интервьюер проверяет, есть ли у вас рабочая ментальная модель того, когда операция блокируется, когда паникует и когда виснет навсегда.
Что обычно проверяют:
- Чем буферизированный channel отличается от небуферизированного — где синхронное рандеву, а где кольцевой буфер.
- Когда именно блокируется отправка и приём — на пустом и на полном буфере.
- Что происходит при операции на
nilchannel — блокировка навсегда, и как это используют вselect. - Что делает отправка в закрытый channel — паника; что возвращает приём — нулевое значение и
ok=false. - Семантику
select— равновероятный выбор, рольdefault, поведение пустогоselect{}. - Как channel приводит к утечке горутины и чем её предотвращают —
selectпоctx.Done()или буферизированный channel результата. - Как
context.Contextраспространяет отмену и как он устроен внутри — деревоcancelCtx/timerCtx/valueCtx.
Типичный неверный ответ: «отправка в закрытый channel просто молча отбрасывается, это безопасно». Это запускает разбор того, что отправка в закрытый channel немедленно паникует, что закрытие — ответственность ровно одного владельца-отправителя, а получатель закрытие лишь обнаруживает.