Как использовать пакет context для передачи сигнала закрытия 15 горутинам в Go
Что проверяют
Понимаете ли вы, что отмена в Go кооперативная: контекст ничего не убивает, он лишь закрывает канал, а горутины обязаны сами на это реагировать. Следом спросят, как дождаться завершения всех пятнадцати и как вернуть ошибку первой упавшей.
Как отвечать
Каркас такой: один родительский контекст, одна функция отмены, общий sync.WaitGroup.
- создание:
ctx, cancel := context.WithCancel(context.Background())и сразуdefer cancel(), чтобы ресурсы контекста освободились при любом выходе; - передача:
ctxпервым аргументом в каждую горутину, по соглашению его не хранят в полях структур; - реакция: в цикле воркера
selectс веткойcase <-ctx.Done(): returnи веткой с реальной работой, например чтением из канала задач. Причину покажетctx.Err():context.Canceledилиcontext.DeadlineExceeded; - ожидание:
wg.Addдо запуска горутины,defer wg.Done()внутри,wg.Wait()послеcancel(). В Go 1.25 появилсяwg.Go(func), который делаетAddиDoneсам; - широковещательность:
Done()возвращает канал, который закрывается, а чтение из закрытого канала срабатывает мгновенно у всех пятнадцати сразу. Поэтому одногоcancel()достаточно, слать пятнадцать значений не нужно.
Хорошо дополнить вариантами из практики. signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM) даёт контекст, который отменится по сигналу ОС: это стандартный способ graceful shutdown. errgroup.WithContext из golang.org/x/sync отменяет контекст при первой ошибке и возвращает её из Wait(). context.WithCancelCause позволяет передать причину отмены.
Где спотыкаются
Делают select с веткой default и time.Sleep внутри. Горутина видит отмену только между «снами», а без работы крутит пустой цикл. Правильнее ждать в том же select канал задач или таймер.
Вызывают wg.Add(1) внутри самой горутины. Тогда Wait() может вернуться раньше, чем горутина успела себя посчитать.
Забывают, что блокирующий вызов отмену не видит. Если воркер висит на чтении из канала без select или на сетевом запросе без ctx, он не завершится, и wg.Wait() зависнет навсегда.