Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».
1. Worker pool: как ограничить число одновременных обработчиков задач?
Классика: N горутин-воркеров читают задачи из одного канала jobs до его закрытия, пишут результаты в results, а главный закрывает jobs и через WaitGroup дожидается воркеров. Канал сам раздаёт задачи — без распределителя. Порядок сборки: (1) jobs := make(chan Job), (2) запустить N воркеров с defer wg.Done(), (3) в отдельной горутине наполнить jobs и close(jobs), (4) читатель results собирает по закрытию. Ошибка новичков — наполнять jobs из main синхронно при небуферизованном канале: main заблокируется на первой задаче, воркеры не начнут работу.
jobs := make(chan Job)
results := make(chan Result)
var wg sync.WaitGroup
for i := 0; i < 8; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
go func() {
defer close(jobs)
for _, j := range allJobs { jobs <- j }
}()
go func() { wg.Wait(); close(results) }()
for r := range results { collect(r) }2. Что такое fan-out и fan-in? Как слить результаты нескольких каналов в один?
Fan-out — раздать работу нескольким горутинам, читающим из одного канала (те же воркеры). Fan-in — слить несколько каналов-результатов в один. Наивный fan-in — горутина на каждый вход, пересылающая значения в общий выход; для честного закрытия общего выхода считают активные источники (sync.WaitGroup) и закрывают его по нулю. Готовая реализация — golang.org/x/sync/errgroup в связке с общим каналом. В ответе ключ: fan-in без подсчёта источников либо закрывает выход рано (потеря данных), либо не закрывает никогда (утечка читателя).
func fanIn(ins ...<-chan Result) <-chan Result {
out := make(chan Result)
var wg sync.WaitGroup
wg.Add(len(ins))
for _, in := range ins {
in := in
go func() {
defer wg.Done()
for v := range in { out <- v }
}()
}
go func() { wg.Wait(); close(out) }()
return out
}3. Как собрать pipeline из стадий и кто закрывает каналы между стадиями?
Pipeline — цепочка стадий: каждая читает из входного канала, преобразует и пишет в выходной, возвращая <-chan Результат. Правило владения: канал закрывает ТОТ, кто в него пишет. Стадия закрывает свой выход при исчерпании входа (for v := range in { out <- f(v) }; close(out)) — закрытие «протекает» по конвейеру от источника к стоку. Разрыв этого правила — двойной close (паника) или незакрытый выход (утечка). Второй вопрос в довесок: что делать, если потребитель уходит раньше времени (ошибка в середине конвейера) — нужен механизм отмены всех стадий: контекст или done-канал, иначе продюсеры зависнут на записи в брошенный выход.
func stage(in <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for v := range in { out <- v * 2 }
}()
return out
}4. Канал done: как остановить горутину без context?
Идиома done-канала: done := make(chan struct{}), горутина в select ждёт либо работу, либо <-done, владелец закрывает done (close — широковещательный сигнал всем читателям сразу). Это предшественник context.WithCancel: сегодня для новой продукции берут контекст, а done-канал живёт в библиотечных API и легаси. Частный полезный паттерн в том же семействе — stop-канал с буфером в 1: close(stop) позволяет горутине «подхватить» сигнал, даже если она занята, и не блокирует отправителя (отправка в буферизованный канал с местом не блокируется).
done := make(chan struct{})
go func() {
for {
select {
case job := <-jobs:
process(job)
case <-done:
return
}
}
}()
close(done) // сигнал остановки всем подписчикам5. Как из канала читать с таймаутом или «первое из двух»?
select по двум веткам: значение из канала и time.After(d). Для «первого успешного из нескольких гонок» — тот же select по нескольким каналам-результатам (паттерн «первый ответ выигрывает»; проигравшие горутины должны завершаться — их результаты уходят в отброшенный канал, и важно не оставить их вечно пишущими: буфер в 1 на канал-результат даёт проигравшему закончить без читателя). Нюанс Go 1.23+: таймеры time.After больше не удерживают память до срабатывания — проблема «time.After в горячем цикле» ушла, но знать о ней спросят и в 2026.
select {
case res := <-fast:
return res
case res := <-slow:
return res
case <-time.After(2 * time.Second):
return ErrTimeout
}6. Когда канал НЕ нужен? Что использовать вместо?
Сильный закрытийный ответ: каналы — про передачу владения данными и координацию, а не про всё подряд. Для защиты общих данных — sync.Mutex/RWMutex проще и быстрее (нет копирования в канал); для счётчиков и флагов — sync/atomic; для «дождаться пачки горутин с ошибками» — errgroup; для кэша — sync.Map или мьютекс. Практическое правило для ответа: если данные не «переходят из рук в руки», а «общие с охраной» — мьютекс; если задачу нужно кому-то доставить — канал. Это размышление показывает зрелость сильнее перечисления синтаксиса.
7. Как узнать, сколько элементов осталось в канале?
len(ch) возвращает число непрочитанных элементов в буфере, cap(ch) — ёмкость. Но строить на них логику — антипаттерн: значение устаревает в момент чтения (другая горутина могла всё забрать), и код на len(ch) построен на гонке по времени. Легитимные применения — диагностика, метрики (лаг очереди в мониторинге), отладочные логи. Правильная логика вместо len — сигналы через закрытие канала, счётчики под мьютексом или контекст. Интервьюер этим вопросом отличает «писал с каналами» от «читал о каналах».
Прокачать тему в самоучителе
Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице:
- Продвинутые возможности языка Go — Продвинутый курс Go: замыкания, defer, горутины, каналы, контекст, интерфейсы, дженерики, рефлексия, тесты и JSON. 25 уроков с практикой в браузере.
- Asynq: полный курс по фоновым задачам в Go — Курс asynq для Go: клиенты и воркеры, отложенные и периодические задачи, ретраи, очереди с приоритетами, агрегация, Inspector и asynqmon. 25 уроков.