> Почему для закрытия канала и ожидания работы горутин используется отдельная горутина, а не main функция? (Go)

Уровень: senior · Роль: backend · Язык: Go · Категория: Технические вопросы

Компании: Aston

Стек: Go

> Пример ответа

Короткий ответ

Потому что закрытие канала и ожидание завершения горутин в main функции напрямую создаёт риск deadlock'а: main не может одновременно закрывать канал и ждать горутины, которые могут писать в этот канал. Отдельная горутина позволяет разнести во времени закрытие канала и ожидание завершения, гарантируя, что все отправители завершатся до закрытия, а main продолжит работу без блокировки.

Подробное объяснение

В Go закрытие канала - это сигнал получателям, что данные больше не будут отправлены. Если закрыть канал, пока горутины ещё пишут в него, произойдёт panic: send on closed channel. Поэтому закрытие должно происходить строго после завершения всех отправителей.

Когда мы используем sync.WaitGroup для ожидания горутин, возникает проблема: если main вызывает wg.Wait() до закрытия канала, а горутины блокируются на отправке в небуферизованный канал, который никто не читает - deadlock. Если main закрывает канал до wg.Wait(), горутины могут ещё писать - panic.

Решение - вынести закрытие канала и ожидание в отдельную горутину:

GO
func main() {
ch := make(chan int)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
ch <- id
}(i)
}
go func() {
wg.Wait()
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
}

Здесь отдельная горутина ждёт завершения всех отправителей и только потом закрывает канал. Main в это время активно читает из канала, не блокируя отправителей. Это классический паттерн fan-in с гарантией безопасного закрытия.

На практике

В реальном коде этот паттерн используется повсеместно: пулы воркеров, обработка очередей, конвейеры. Альтернативы - использование context.Context для отмены или канал done для сигнализации, но для простого сценария "запустили N горутин, собрали результаты" отдельная горутина с WaitGroup - стандарт.

Важно помнить: закрывать канал должен только отправитель. Если несколько отправителей - нужен механизм синхронизации (WaitGroup, sync.Once, отдельная горутина). Получатель никогда не закрывает канал.

Пример кода

GO
package main
import (
"fmt"
"sync"
)
func worker(id int, ch chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
ch <- id * 2
}
func main() {
const n = 5
ch := make(chan int, n) // буфер для избежания блокировки
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go worker(i, ch, &wg)
}
// отдельная горутина: ждём всех и закрываем
go func() {
wg.Wait()
close(ch)
}()
for val := range ch {
fmt.Println(val)
}
}

Без буфера этот код тоже работает, потому что main читает канал параллельно с отправкой. Буфер здесь - для наглядности, но не обязателен.

Как отвечать на собеседовании

Начни с проблемы: "если закрыть канал в main до завершения горутин - panic, если ждать в main - deadlock". Затем объясни, что отдельная горутина решает обе проблемы: она ждёт завершения отправителей и закрывает канал, а main в это время потребляет данные. Упомяни, что это стандартный паттерн синхронизации в Go, и что закрывать канал должен только отправитель. Если спросят про альтернативы - скажи про sync.Once или канал done, но подчеркни, что для массового ожидания горутин WaitGroup + отдельная горутина - идиоматичный выбор.

Что проверяет интервьюер

Интервьюер проверяет понимание модели памяти Go, правил работы с каналами (кто закрывает, кто отправляет), умение избегать deadlock и panic. Также оценивается знание идиоматичных паттернов конкурентности и способность объяснить, почему именно такое решение, а не "просто закрыть в main". Это тест на senior-уровень: кандидат должен видеть не только код, но и причины, по которым он устроен именно так.

Типичные ошибки

  • Закрытие канала в main до wg.Wait() - panic: send on closed channel.
  • Вызов wg.Wait() в main без отдельной горутины - deadlock при небуферизованном канале.
  • Закрытие канала получателем - нарушение правила "только отправитель закрывает".
  • Использование defer close(ch) в горутине-отправителе при нескольких отправителях - panic при повторном закрытии.
  • Забыть wg.Add(1) перед go - гонка, WaitGroup может завершиться раньше, чем горутина начнёт работу.

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью