Когда обычно закрывают горутины, работающие в бесконечных циклах в Go
Что проверяют
Знаете ли вы, что горутину нельзя остановить снаружи: она завершается только сама, поэтому у каждой долгоживущей горутины должен быть владелец и способ сказать ей «выходи». Следом обычно спрашивают, как дождаться завершения при остановке сервиса.
Как отвечать
Бесконечный цикл завершают в четырёх ситуациях: при остановке приложения, по окончании запроса или операции-владельца, при закрытии входного канала и при фатальной ошибке. Механизмы под это:
context.Context: цикл строится вокругselectсcase <-ctx.Done(): return. Для остановки сервиса контекст получают черезsignal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM);- закрытие канала:
close(done)будит всех читателей сразу, аfor v := range jobsвыходит сам, когда производитель закрылjobs; - ожидание:
sync.WaitGroupилиerrgroup.WithContextизgolang.org/x/sync, который при первой ошибке отменяет контекст остальных и возвращает её изWait().
Периодическую работу пишут без default и time.Sleep:
t := time.NewTicker(time.Second)
defer t.Stop()
for {
select {
case <-ctx.Done():
return
case <-t.C:
doWork(ctx)
}
}Так горутина реагирует на отмену сразу, а не после очередного сна. Сам ctx передают и внутрь работы: блокирующий вызов сети или базы без контекста отмену не заметит, и цикл застрянет внутри итерации.
При graceful shutdown порядок такой: отменить контекст, дождаться горутин с таймаутом, закрыть соединения и выйти из main. Утечки горутин в тестах ловит go.uber.org/goleak.
Где спотыкаются
Пишут select с пустым default. Цикл превращается в активное ожидание и съедает ядро целиком.
Не ждут горутины перед выходом. Когда main возвращается, программа завершается, и горутины обрываются посреди записи в файл или базу.
Отправляют результат в канал без select с ctx.Done(). Если читатель уже ушёл, горутина висит на отправке вечно, и это та же утечка, от которой пытались уйти.