Канал передаёт значения и синхронизирует горутины. Закрывает тот, кто пишет. Send в закрытый канал — panic. Receive из закрытого — нуль типа и ok=false.
Конкурентность Go golang собеседование
Канал — типизированная очередь с правилами блокировки. Небуферизованный канал (ёмкость 0) делает рукопожатие: отправитель и получатель встречаются, иначе кто-то ждёт. Буфер N позволяет отправить до N значений без готового читателя. Дальше send снова блокируется. Это не бесконечная очередь и не замена мьютексу на каждый чих.
В сервисах каналами ограничивают пул воркеров, сигналят о завершении, собирают значения из нескольких источников. Легко получить взаимную блокировку: все ждут send, читателя нет. Или закрыли канал, а другой писатель ещё шлёт. Поэтому владение каналом должно быть однозначным.
Аналогия: труба между комнатами. Без буфера передача — из рук в руки: оба должны быть у трубы. С буфером N — на полке у трубы лежат до N коробок. Кто кладёт коробки, тот говорит «больше не будет» и закрывает трубу.
Буфер, close и range
Закрывает писатель, когда значений больше не будет. Читатели не закрывают: следующий send от другого писателя паникует. Несколько писателей закрывают только после синхронизации (WaitGroup), один раз. Повторный close — тоже panic.
Receive из закрытого канала не блокируется: сразу нуль типа, второй результат ok == false. range ch читает, пока канал не закроют. Забыть закрыть — range и читатель висят навсегда. Send в закрытый — panic, не error.
func fanIn(ctx context.Context, a, b <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for a != nil || b != nil {
select {
case <-ctx.Done():
return
case v, ok := <-a:
if !ok {
a = nil
continue
}
out <- v
case v, ok := <-b:
if !ok {
b = nil
continue
}
out <- v
}
}
}()
return out
}
Писатель закрывает канал. Исчерпанный источник в select отключают через nil.
Пример. Воркер читает for job := range jobs. Прод забыл close(jobs) после последней задачи. Воркер висит, WaitGroup не доходит до нуля, shutdown не завершается.
Нулевой канал (var ch chan T) блокирует и send, и receive навсегда. В select это полезно: присвоить case-каналу nil, чтобы отключить ветку, не вынимая её из select.
Владение и направления
В сигнатуре chan<- T — только send, <-chan T — только receive. Так компилятор не даст читателю закрыть канал. Создатель канала обычно пишет и закрывает, наружу отдаёт только чтение. Буфер ставят, когда знают оценку очереди, например число воркеров. Безразмерный буфер «на всякий случай» прячет давление очереди и копится в памяти.
Небуферизованный send — точка синхронизации: пока никто не принял, отправитель не идёт дальше. Это полезно, когда передаёте владение объектом. После send без буфера получатель видит значение. Отправитель по договорённости больше не трогает тот же буфер.
Пример. Пул из 8 воркеров. Канал задач с буфером 8. Быстрый продюсер упрётся в полный буфер и подождёт. Буфер на миллион «чтобы не блокировалось» съест память при всплеске.
Типичная ошибка
Закрыть канал со стороны читателя. Или закрыть его при нескольких писателях без WaitGroup — panic на send или на втором close. Вторая: ждать, что send в закрытый вернёт error. Третья — забыть close, и range в другой горутине не завершится.
На собеседовании
Кто пишет, тот закрывает. Receive из закрытого — нуль и ok=false. Send в закрытый — panic. Nil-канал отключает case в select. Буфер ограничивает очередь, а не делает send всегда свободным.