> Когда обычно закрывают горутины, работающие в бесконечных циклах в Go (Go)

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

Компании: ЦУМ

Стек: Go

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

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

Горутины в бесконечных циклах закрывают через контекст (context.Context) или отдельный канал завершения (done channel). Основной паттерн - select внутри цикла, который слушает сигнал отмены и выход из цикла. Закрытие происходит при завершении работы приложения, graceful shutdown, или когда задача больше не нужна. Важно не закрывать горутину извне принудительно - Go не предоставляет механизма остановки горутины, только cooperative cancellation.

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

В Go нет API для принудительного завершения горутины. Единственный способ - cooperative cancellation: горутина сама проверяет сигнал на завершение и выходит из цикла. Стандартные механизмы:

  1. context.Context - рекомендуемый способ для большинства случаев. Передаётся в функцию, запускающую горутину. Внутри цикла select с ctx.Done().

  2. Канал done - простой вариант, когда контекст не нужен. Закрытие канала сигнализирует о завершении.

  3. sync.WaitGroup - не для остановки, но для ожидания завершения всех горутин перед выходом из программы.

Когда закрывают:

  • при graceful shutdown сервера (например, http.Server.Shutdown)
  • при завершении работы worker pool
  • когда источник данных иссяк или потребитель больше не нужен
  • при обработке сигналов ОС (SIGINT, SIGTERM)

Типичная структура:

GO
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
// полезная работа
}
}
}

Важно: если горутина выполняет блокирующую операцию (например, чтение из канала), select должен включать и эту операцию, и ctx.Done().

На практике

Для backend-приложений на Go паттерн выглядит так:

  • В main: создаётся корневой контекст с signal.NotifyContext, который отменяется при получении SIGINT/SIGTERM.
  • В серверах: http.Server.Shutdown(ctx) сам останавливает обработчики, но фоновые горутины нужно останавливать вручную через тот же контекст.
  • В worker pool: каждый worker получает контекст, при отмене - завершает цикл, затем wg.Done().
  • В библиотеках: экспортируемые функции принимают контекст первым аргументом - это конвенция Go.

Практические рекомендации:

  • всегда передавайте контекст в горутины, даже если сейчас отмена не нужна - это дёшево и упрощает будущие изменения
  • не используйте runtime.Goexit() или panic для остановки горутин - это опасно и не идиоматично
  • при использовании select с default в цикле добавляйте time.Sleep или runtime.Gosched() для снижения нагрузки на CPU
  • для длительных операций внутри цикла проверяйте ctx.Err() между шагами

Пример кода

GO
package main
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("worker %d stopped: %v\n", id, ctx.Err())
return
default:
// имитация работы
time.Sleep(100 * time.Millisecond)
fmt.Printf("worker %d working\n", id)
}
}
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
<-ctx.Done()
fmt.Println("shutdown signal received, waiting for workers...")
wg.Wait()
fmt.Println("all workers stopped")
}

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

Начните с ключевого тезиса: горутины не закрываются извне, они завершаются сами через cooperative cancellation. Затем упомяните два основных механизма - контекст и канал done. Объясните, почему контекст предпочтительнее: он стандартизирован, поддерживает дедлайны и значения. Приведите пример graceful shutdown: сигнал ОС → отмена контекста → горутины выходят из циклов → WaitGroup дожидается завершения. Подчеркните важность проверки ctx.Done() именно в select, а не только в начале цикла - это позволяет реагировать на отмену во время блокирующих операций. Если спросят про принудительную остановку - скажите, что такого механизма нет, и это осознанное решение дизайна Go.

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

Интервьюер оценивает:

  • понимание модели конкурентности Go и отсутствие механизма принудительного завершения горутин
  • знание идиоматичных паттернов отмены: контекст, каналы, select
  • умение проектировать graceful shutdown в реальном приложении
  • понимание роли sync.WaitGroup для синхронизации завершения
  • способность объяснить trade-off между проверкой ctx.Done() в select и в начале цикла
  • знание конвенции передачи контекста первым аргументом

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

  • попытка убить горутину через runtime.Goexit() или закрытие канала, из которого она читает - это приводит к панике или утечкам
  • отсутствие проверки ctx.Done() внутри цикла, только в начале - горутина не отреагирует на отмену во время длительной работы
  • использование select с default без time.Sleep - 100% загрузка CPU
  • забывание wg.Done() при возврате из горутины - deadlock при wg.Wait()
  • создание контекста внутри горутины вместо передачи извне - отмена не сработает
  • игнорирование возвращаемого cancel из context.WithCancel - утечка ресурсов
  • завершение программы без ожидания горутин - данные теряются, соединения не закрываются

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

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