Каналы
Механика каналов, запись в закрытый канал, select и пакет context.
21 вопросов
JuniorТеорияОчень частоЧто такое channel, и чем буферизированный channel отличается от небуферизированного?
Что такое channel, и чем буферизированный channel отличается от небуферизированного?
Channel — это типизированный канал передачи значений между горутинами. Небуферизированный синхронизирует: отправка блокируется, пока не готов получатель. Буферизированный (make(chan T, n)) хранит до n значений, и отправка блокируется только когда буфер заполнен.
Типичные ошибки
- ✗Считать, что буферизированный channel никогда не блокирует отправителя — он блокируется при заполнении буфера
- ✗Думать, что небуферизированный channel имеет буфер размера один, а не ноль
- ✗Предполагать, что channels разделяют память, а не передают копии значений между горутинами
Уточняющие вопросы
- →Какое значение и флаг
okвозвращает приём после закрытия channel? - →Почему отправка в небуферизированный channel устанавливает отношение happens-before с приёмом?
JuniorТеорияОчень частоКак отправлять и принимать значения по каналу в Go?
Как отправлять и принимать значения по каналу в Go?
Канал, созданный через make(chan T), — это типизированный канал передачи между горутинами. Отправка пишется как ch <- v, а приём — как v := <-ch; стрелка всегда указывает в направлении движения значения. На небуферизированном канале отправка блокируется, пока другая горутина не будет готова принять, а приём блокируется, пока значение не придёт, поэтому обе горутины встречаются и синхронизируются в этой точке. Тип элемента T фиксируется при создании.
Типичные ошибки
- ✗Направлять стрелку
<-не в ту сторону для отправки и приёма - ✗Считать, что небуферизированная отправка вернётся до готовности получателя
- ✗Забывать, что тип элемента канала фиксируется при
make
Уточняющие вопросы
- →Что сообщает форма с запятой
v, ok := <-ch? - →Как приём устанавливает отношение happens-before с отправкой?
MiddleТеорияОчень частоКак буферизированный канал меняет момент блокировки отправки по сравнению с небуферизированным?
Как буферизированный канал меняет момент блокировки отправки по сравнению с небуферизированным?
Небуферизированный канал имеет ёмкость 0, поэтому каждая отправка блокируется, пока получатель не будет готов забрать значение — отправка и приём должны встретиться. Буферизированный канал из make(chan T, n) хранит до n значений, поэтому первые n отправок завершаются без ожидающего получателя и блокируются, только когда буфер заполнен. Это развязывает отправителя и получателя на n элементов: производитель может забегать вперёд, сглаживая всплески, но не получает неограниченного запаса.
Типичные ошибки
- ✗Думать, что небуферизированный канал имеет одну ячейку буфера
- ✗Считать, что буферизированная отправка никогда не блокирует отправителя
- ✗Полагать, что заполненный буфер отбрасывает отправки, а не блокирует
Уточняющие вопросы
- →Когда вы намеренно выберете небуферизированный канал вместо буферизированного?
- →Что сообщают
cap(ch)иlen(ch)для буферизированного канала?
MiddleТеорияОчень частоКакова семантика select, включая ветку default?
Какова семантика select, включая ветку default?
select ждёт, пока одна из его channel-веток не станет готовой; если готовы несколько, он выбирает одну равновероятно случайно. С веткой default он никогда не блокируется — если ни одна ветка не готова, сразу выполняется default. Пустой select{} блокируется навсегда.
Типичные ошибки
- ✗Считать, что ветки
selectприоритизируются по порядку — выбор равновероятно случайный - ✗Думать, что ветка
defaultзаставляетselectопрашивать в цикле, а не возвращаться сразу - ✗Забывать, что пустой
select{}блокирует горутину навсегда
Уточняющие вопросы
- →Как реализовать приём с таймаутом через
selectиtime.After? - →Как
selectпо channelctx.Done()позволяет горутине реагировать на отмену?
JuniorТеорияЧастоКак определить при приёме, что channel был закрыт?
Как определить при приёме, что channel был закрыт?
Используйте форму comma-ok v, ok := <-ch: ok равно false, когда channel закрыт и опустошён, а v — нулевое значение типа элемента. Идиоматичная альтернатива — цикл for range ch: он завершается автоматически при закрытии channel.
Типичные ошибки
- ✗Выдумывать несуществующую встроенную функцию
closed(ch)— в Go такого предиката нет - ✗Думать, что приём из закрытого channel паникует — паникует только отправка
- ✗Проверять значение на равенство нулевому для обнаружения закрытия вместо использования
ok
Уточняющие вопросы
- →Почему закрытый channel всё ещё отдаёт буферизированные значения до того, как
okстанет false? - →Как ветка
selectведёт себя с закрытым channel при приёме?
JuniorТеорияЧастоЧто происходит при отправке в nil channel или приёме из него?
Что происходит при отправке в nil channel или приёме из него?
И отправка, и приём на nil channel блокируются навсегда — операция никогда не продолжается и не паникует. Channel-переменная с нулевым значением равна nil. Это иногда применяют намеренно в select, отключая ветку присваиванием её channel значения nil.
Типичные ошибки
- ✗Думать, что операция на
nilchannel паникует, а не блокируется навсегда - ✗Путать
nilchannel с закрытым каналом — закрытый не блокируется - ✗Забывать, что объявленная, но не созданная через
makechannel-переменная равнаnil
Уточняющие вопросы
- →Как присваивание channel значения
nilв цикле помогает отключить веткуselect? - →Чем поведение
nilchannel отличается от закрытого при приёме?
JuniorТеорияЧастоЧто происходит при range по каналу и когда он закрыт?
Что происходит при range по каналу и когда он закрыт?
for v := range ch продолжает принимать значения одно за другим и завершается только когда канал закрыт и опустошён. close(ch) — это сигнал отправителя, что значений больше не будет; range по незакрытому каналу блокируется навсегда, как только тот опустеет. Приём из закрытого канала возвращает нулевое значение элемента с ok == false, что и останавливает цикл. Закрывать должен только отправитель, а повторное закрытие или отправка после закрытия вызывает панику.
Типичные ошибки
- ✗Считать, что цикл
rangeзавершается, когда канал просто пуст - ✗Думать, что приём из закрытого канала возвращает
ok == true - ✗Позволять получателю закрывать канал вместо отправителя
Уточняющие вопросы
- →Как определить закрытый канал формой с запятой?
- →Почему отправка в закрытый канал паникует, а приём — нет?
MiddleКодЧастоПриём из закрытого channel, отправка в него и повторное закрытие — предскажите каждую
Приём из закрытого channel, отправка в него и повторное закрытие — предскажите каждую
Приём из закрытого channel возвращает нулевое значение с ok == false, поэтому (1) даёт v == 0, ok == false. Отправка в закрытый channel паникует с send on closed channel, поэтому (2) паникует. Повторное закрытие закрытого channel тоже паникует, поэтому (3) паникует. Правило — закрывает отправитель, никогда не получатель, и только один раз.
Типичные ошибки
- ✗Думать, что приём из закрытого channel блокируется, а не возвращает нулевое значение с
ok == false - ✗Считать, что закрытый channel всё ещё принимает отправки — он паникует
- ✗Полагать, что
closeидемпотентен — повторное закрытие паникует
Уточняющие вопросы
- →Как безопасно скоординировать закрытие, когда отправителей несколько?
- →Почему
rangeпо закрытому channel завершается чисто, а одиночный приём возвращает нули бесконечно?
MiddleТеорияЧастоДля чего нужен context.Context и как распространяется отмена?
Для чего нужен context.Context и как распространяется отмена?
context.Context переносит сигналы отмены, дедлайны и значения уровня запроса через границы API. Вызов функции cancel или наступление дедлайна закрывает channel Done() контекста; это закрытие распространяется на каждый производный дочерний контекст, и все они видят отмену.
Типичные ошибки
- ✗Думать, что отмена идёт от ребёнка к родителю, а не от родителя ко всем детям
- ✗Считать, что отмена контекста принудительно убивает использующие его горутины
- ✗Забывать вызвать функцию
cancel, из-за чего утекают ресурсы контекста
Уточняющие вопросы
- →Почему функцию
cancelнужно вызывать всегда, даже если контекст уже завершился? - →Как контекст
WithDeadlineпревращает лимит времени в закрытие channelDone()?
MiddleТеорияЧастоКак channel устроен внутри — что хранит runtime-структура hchan?
Как channel устроен внутри — что хранит runtime-структура hchan?
Channel — это указатель на runtime-структуру hchan: кольцевой буфер (buf с индексами sendx/recvx и счётчиками элементов), две очереди ожидания заблокированных горутин (sendq, recvq) и mutex. Каждая отправка/приём берёт лок; если операция не может пройти, горутина паркуется в очереди и позже будится своим партнёром. Закрытие выставляет флаг и будит обе очереди.
Типичные ошибки
- ✗Считать channels lock-free — каждая операция берёт mutex структуры
hchan - ✗Думать, что заблокированные отправка/приём крутятся в busy-wait, а не паркуют горутину в очереди ожидания
- ✗Воображать буфер безграничным, а не фиксированным кольцом, размер которого задан при
make
Уточняющие вопросы
- →Как отправка в небуферизированный channel передаёт значение прямо ждущему получателю, не трогая
buf? - →Что происходит с горутинами, припаркованными в
recvq, при закрытии channel?
JuniorКодИногдаЧто произойдёт при отправке в небуферизированный channel до приёма?
Что произойдёт при отправке в небуферизированный channel до приёма?
Будет deadlock: fatal error: all goroutines are asleep - deadlock!. Отправка в небуферизированный channel блокируется, пока не готов получатель, но единственная горутина заблокирована на этой отправке, поэтому приём не выполняется. Исправление: отправлять из отдельной горутины или make(chan int, 1).
Типичные ошибки
- ✗Думать, что небуферизированный channel буферизует одно значение — его ёмкость ноль
- ✗Ожидать, что runtime тихо зависнет, а не сообщит
deadlock! - ✗Считать, что одна горутина может последовательно и отправить, и принять на небуферизированном channel
Уточняющие вопросы
- →Почему runtime Go может обнаружить этот deadlock, но не тот, что связан с заблокированным syscall?
- →Как перенос отправки в
go func(){ ch <- 1 }()исправляет ситуацию?
MiddleТеорияИногдаЧто происходит при отправке в закрытый channel, и как этого избежать?
Что происходит при отправке в закрытый channel, и как этого избежать?
Отправка в закрытый channel немедленно паникует с send on closed channel. Избегайте этого, назначив одного владельца ответственным за закрытие и никогда не закрывая из получателя или из нескольких отправителей. Закрытие решает отправитель, получатель лишь обнаруживает его через comma-ok.
Типичные ошибки
- ✗Закрывать channel со стороны получателя, а не из отправителя-владельца
- ✗Вызывать
closeиз нескольких отправителей, что само паникует при двойном закрытии - ✗Считать, что отправка в закрытый channel молча отбрасывается, а не паникует
Уточняющие вопросы
- →Как
sync.WaitGroupпомогает определить, когда можно закрыть fan-in channel? - →Почему повторный вызов
closeна том же channel тоже паникует?
MiddleДебаггингИногдаПочему программа уходит в deadlock, когда горутина проходит range по channel, который никогда не закрывают?
Почему программа уходит в deadlock, когда горутина проходит range по channel, который никогда не закрывают?
for v := range ch принимает значения, пока channel не закрыт. Продьюсер отправляет 1 и 2, но никогда не вызывает close(ch), поэтому потребитель навсегда блокируется после их вычерпывания, wg.Done() не выполняется, и wg.Wait() тоже блокируется — deadlock. Исправление: close(ch) после последней отправки.
Типичные ошибки
- ✗Думать, что
range chостанавливается, когда channel пуст, а не когда он закрыт - ✗Закрывать channel из получателя вместо отправителя
- ✗Забывать, что незакрытый channel держит горутину
rangeживой, поэтомуwg.Wait()не возвращается
Уточняющие вопросы
- →Кто отвечает за закрытие channel и почему никогда не получатель?
- →Как
selectс каналомdoneпозволил бы потребителю выйти без закрытия?
MiddleКодИногдаЧто напечатает этот цикл select по channel ёмкости 1?
Что напечатает этот цикл select по channel ёмкости 1?
Печатает 02468. С ёмкостью 1 на одной goroutine ветки отправки и приёма чередуются: при пустом буфере готова лишь отправка (кладёт i, ничего не печатает); на следующей итерации буфер полон, поэтому готов лишь приём (печатает сохранённое чётное значение). С небуферизированным channel ни одна ветка никогда не готова на одной goroutine, поэтому это fatal error: all goroutines are asleep - deadlock.
Типичные ошибки
- ✗Думать, что один проход
selectможет и отправить, и принять по одному channel - ✗Забывать, что небуферизированный channel блокирует обе ветки, вызывая deadlock
- ✗Называть deadlock восстановимой panic, а не фатальной runtime-ошибкой
Уточняющие вопросы
- →Почему самая первая итерация ничего не печатает, а не
0? - →Почему
all goroutines are asleep - deadlock— фатальная ошибка, а неpanic, которую можноrecover?
MiddleКодИногдаПочему эта программа с двумя worker занимает ~6с, а не ~3с?
Почему эта программа с двумя worker занимает ~6с, а не ~3с?
Печатает 6. Go вычисляет выражение с двумя операндами слева направо: первый <-worker() вызывает worker() (запускает goroutine №1) и затем блокируется на 3с на приёме из него. Только после этого вообще стартует второй worker(), и блокируется ещё на 3с — они идут последовательно, всего ~6с. Чтобы совместить, сначала запустите обе: c1, c2 := worker(), worker(); <-c1; <-c2 → ~3с.
Типичные ошибки
- ✗Считать, что Go запускает оба вызова
worker()до любого приёма - ✗Думать, что отбрасывание через
_пропускает блокирующий приём - ✗Полагать, что совместить работу может только буфер, а не перестановка запусков
Уточняющие вопросы
- →Каков в Go порядок вычисления операндов многозначного выражения?
- →Изменит ли буферизация channel результат ~6с, и почему да или нет?
MiddleКодИногдаРеализуйте Sleep, который выходит раньше, если его context.Context отменён
Реализуйте Sleep, который выходит раньше, если его context.Context отменён
select по двум case: case <-ctx.Done(): return false и case <-timer.C: return true. Не используйте time.After, который течёт нижележащим таймером до срабатывания; вместо этого t := time.NewTimer(d); defer t.Stop(), чтобы при отмене таймер освобождался сразу.
Типичные ошибки
- ✗Использовать
time.Sleep, а потом проверять контекст, что не даёт вернуться до истечения всей длительности - ✗Использовать
time.Afterв select, который течёт таймером до срабатывания, если контекст отменён первым - ✗Добавлять лишнюю goroutine, когда один таймер плюс
ctx.Done()уже покрывают оба случая
Уточняющие вопросы
- →Почему
time.Afterтечёт своим таймером, аtime.NewTimerплюсStop— нет? - →Создаёт ли проблему вызов
Stopна таймере, который уже сработал, в этом случае?
MiddleКодИногдаОбернуть медленную функцию таймаутом через channel и select
Обернуть медленную функцию таймаутом через channel и select
Запустите unpredictableFunc в goroutine, которая шлёт результат в буферизированный channel ёмкости 1, затем select между этим channel и ctx.Done(). Буфер здесь несущий: с небуферизированным channel worker навсегда блокируется на отправке после таймаута, утекая. Если у ctx нет дедлайна, оберните его context.WithTimeout с defer cancel().
Типичные ошибки
- ✗Использовать небуферизированный channel и утечь goroutine worker при таймауте
- ✗Считать, что отмена
contextсама проникает в непрозрачный блокирующий вызов - ✗Забыть
defer cancel()послеcontext.WithTimeoutи утечь таймер
Уточняющие вопросы
- →Почему небуферизированный channel результата приводит к утечке worker после таймаута?
- →Почему
context.WithTimeoutтребует парного вызоваcancel()?
SeniorТеорияРедкоКак устроен context.Context — channel Done, дедлайн, цепочка значений?
Как устроен context.Context — channel Done, дедлайн, цепочка значений?
cancelCtx хранит лениво создаваемый channel Done и набор дочерних контекстов; cancel закрывает этот channel и рекурсивно отменяет детей. timerCtx оборачивает его time.Timer, который вызывает cancel по дедлайну. valueCtx — один узел ключ/значение, и поиск идёт вверх по цепочке родителей.
Типичные ошибки
- ✗Думать, что
Doneотправляет значение при отмене, а не закрывает channel для всех ждущих - ✗Считать, что channel
Doneсоздаётся сразу, а не лениво при первом вызове - ✗Полагать, что
WithValueстроит слитую map, а не связанный узел, обходимый при поиске
Уточняющие вопросы
- →Почему закрытие channel
Done— правильный примитив для широковещательной отмены? - →Какие накладные расходы добавляет глубокая цепочка
valueCtxпоискуctx.Value?
SeniorДебаггингРедкоОтладьте эту fan-in-склейку каналов: range merge(...) уходит в deadlock. Найдите обе ошибки.
Отладьте эту fan-in-склейку каналов: range merge(...) уходит в deadlock. Найдите обе ошибки.
Две ошибки. Закрывающая goroutine записана как go func(){ wg.Wait(); close(out) } без завершающих (), поэтому она объявлена, но не запущена — out никогда не закрывается и range merge(...) уходит в deadlock. Вторая: замыкание worker захватывает переменную цикла c, поэтому до 1.22 все goroutine читают один и тот же последний channel. Исправление: вызвать закрытие через () и передать c аргументом (или переобъявить).
Типичные ошибки
- ✗Читать
go func(){...}как запущенную goroutine, хотя без()это лишь объявленное, никогда не вызванное значение - ✗Считать, что до 1.22 переменная цикла своя на итерацию, поэтому захваченный
c— последний channel для каждого worker - ✗Винить в deadlock гонку
closeс отправкой вместо того, чтоoutникогда не закрывается
Уточняющие вопросы
- →Почему переменная цикла на итерацию в Go 1.22 убирает необходимость передавать
cаргументом? - →Как
go vetили race detector помогут поймать захват переменной цикла до runtime?
SeniorТеорияРедкоКак channels вызывают утечки горутин, и как их предотвратить?
Как channels вызывают утечки горутин, и как их предотвратить?
Горутина утекает, когда навсегда блокируется на отправке или приёме channel, который никто не удовлетворит — например, worker отправляет результат, когда вызвавший уже вернулся. Давайте каждой блокирующей операции выход: select по ctx.Done() или буферизированный channel результата.
Типичные ошибки
- ✗Думать, что сборщик мусора освободит горутину, навсегда заблокированную на channel
- ✗Запускать worker, отправляющий результат, не дав ему пути выхода через
ctx.Done() - ✗Считать, что утекает только непрочитанный буфер, а не неудовлетворённая отправка или приём
Уточняющие вопросы
- →Как буферизированный channel результата ёмкостью один позволяет брошенному worker выйти?
- →Как обнаружить утечку горутин в тесте или в продакшене?
SeniorКодРедкоНапишите fan-in merge, сводящий N входных каналов в один выходной
Напишите fan-in merge, сводящий N входных каналов в один выходной
Запустите по горутине на каждый входной канал, каждая range-ит свой канал и шлёт все значения в общий out. sync.WaitGroup считает пересыльщиков; отдельная горутина зовёт wg.Wait(), затем close(out), поэтому выход закрывается ровно один раз после опустошения всех входов. out верните сразу. range по входам и закрытие out только после Wait исключают и панику send-on-closed, и утечку горутины.
Типичные ошибки
- ✗Закрывать
outвнутри каждого пересыльщика, вызывая панику send-on-closed в других горутинах - ✗Использовать один
selectи считать, что один приём с канала означает его опустошение - ✗Никогда не закрывать
out, из-за чегоrangeвызывающего блокируется навсегда
Уточняющие вопросы
- →Как добавить отмену, чтобы
mergeостанавливался раньше при отменеcontext.Context? - →Почему
close(out)должен быть в своей горутине, а не после циклаfor, запускающего пересыльщиков?