Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Middle ~20 мин 7 вопросов

Паттерны каналов в Go: worker pool, fan-in, pipeline, done — вопросы с ответами

Отдельно от «что такое канал» интервьюеры спрашивают паттерны: как организовать пул воркеров, слить результаты из многих источников, собрать конвейер с честным закрытием. Это вопросы уровня «расскажите, как бы вы спроектировали», и отвечать на них лучше названиями паттернов и их границами. База — в теме горутины и каналы; живая практика — в продвинутом курсе.

Как отвечать: определение → как работает внутри → где ловушка → пример. Отвечайте вслух до чтения ответа — собеседование это устный жанр, и «понимаю про себя» не равно «объясняю словами».

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 — сигналы через закрытие канала, счётчики под мьютексом или контекст. Интервьюер этим вопросом отличает «писал с каналами» от «читал о каналах».

Прокачать тему в самоучителе

Читается быстро, запоминается практикой. Эти курсы закрывают тему с запуском кода прямо на странице: