> Как использовать пакет context для передачи сигнала закрытия 15 горутинам в Go (Go)

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

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

Стек: Go

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

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

Для передачи сигнала закрытия 15 горутинам используйте context.WithCancel - создайте один контекст, передайте его во все горутины, а при необходимости остановки вызовите cancel(). Каждая горутина должна слушать ctx.Done() в select или проверять его в цикле. Это стандартный паттерн graceful shutdown, который гарантирует, что все горутины получат сигнал одновременно и смогут корректно завершить работу.

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

Пакет context предоставляет механизм распространения сигналов отмены через всю цепочку вызовов. Основная идея: создаётся родительский контекст с функцией cancel, которая при вызове закрывает канал ctx.Done(). Все горутины, получившие этот контекст (напрямую или через дочерние контексты), могут отслеживать закрытие канала.

Ключевые моменты:

  • context.WithCancel(parent) возвращает ctx и cancel. Вызов cancel() - идиоматичный способ сигнализировать об остановке.
  • Канал ctx.Done() закрывается ровно один раз, и все горутины, слушающие его, получают событие одновременно.
  • Горутина должна явно проверять ctx.Done() - контекст не прерывает выполнение принудительно, он лишь предоставляет сигнал.
  • Важно вызывать cancel() через defer в функции, создающей контекст, чтобы избежать утечки ресурсов, даже если остановка не потребуется.
  • Для передачи значений используйте context.WithValue, но для сигнала закрытия это не требуется - достаточно самого факта отмены.

На практике

При работе с пулом горутин (например, worker pool) контекст передаётся как первый аргумент в каждую функцию. Каждая горутина в своём цикле проверяет select на ctx.Done() и основной канал работы. При получении сигнала горутина завершает текущую операцию (если это возможно) и выходит.

Важно: если горутина блокируется на операции, которая не поддерживает контекст (например, системный вызов), нужно обернуть её в отдельную горутину или использовать таймаут. В реальном коде часто комбинируют context.WithCancel и sync.WaitGroup для ожидания завершения всех горутин после вызова cancel().

Для 15 горутин не нужно создавать 15 отдельных контекстов - один общий контекст передаётся всем. Если требуется остановить только часть горутин, создавайте отдельные дочерние контексты через context.WithCancel от общего.

Пример кода

GO
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("worker %d: stopping, reason: %v\n", id, ctx.Err())
return
default:
// имитация работы
fmt.Printf("worker %d: working\n", id)
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // гарантируем освобождение ресурсов
var wg sync.WaitGroup
for i := 1; i <= 15; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
time.Sleep(500 * time.Millisecond)
cancel() // сигнал всем горутинам
wg.Wait() // ждём завершения
fmt.Println("all workers stopped")
}

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

Начните с краткого определения: context.WithCancel создаёт контекст и функцию отмены. Затем объясните механизм: cancel() закрывает канал Done(), и все горутины, слушающие его, получают сигнал. Подчеркните, что контекст - это кооперативный механизм: горутина сама должна проверять ctx.Done(), принудительного прерывания нет.

Упомяните, что для 15 горутин достаточно одного контекста, и что нужно использовать sync.WaitGroup для ожидания завершения. Расскажите про defer cancel() для предотвращения утечек. Если спросят про альтернативы - упомяните канал chan struct{}, но отметьте, что контекст стандартизирован и поддерживает таймауты и дедлайны.

Хорошо показать понимание edge case: если горутина блокируется на операции без поддержки контекста, нужно оборачивать её или использовать таймаут. Также можно упомянуть context.WithTimeout и context.WithDeadline как расширения базового паттерна.

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

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

  • Понимание кооперативной природы отмены через контекст - что горутина сама должна реагировать на сигнал.
  • Умение правильно создавать и освобождать контекст через defer cancel().
  • Знание паттерна graceful shutdown: сигнал → завершение работы → ожидание через WaitGroup.
  • Понимание разницы между context.Background() и context.TODO().
  • Способность объяснить, почему контекст предпочтительнее простого канала: встроенная поддержка таймаутов, дедлайнов, значений и стандартизация в API.
  • Внимание к деталям: например, что ctx.Err() возвращает причину отмены (context.Canceled или context.DeadlineExceeded).

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

  • Не вызывать cancel() - утечка ресурсов, контекст никогда не будет отменён.
  • Вызывать cancel() несколько раз - это безопасно, но бессмысленно; лучше вызывать один раз.
  • Забывать wg.Wait() - программа завершится до остановки горутин.
  • Проверять ctx.Done() только в начале функции, а не в цикле - горутина не остановится вовремя.
  • Использовать context.WithValue для сигнала отмены - это неверно, для этого есть Done().
  • Передавать context.Background() в каждую горутину вместо общего контекста - тогда отмена не сработает.
  • Блокироваться на операции без поддержки контекста - горутина зависнет, и wg.Wait() будет ждать вечно.

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

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