Как выйти из бесконечного цикла обновления кэша
Что проверяют
Знаете ли вы, как в Go корректно останавливать долгоживущую горутину: через context, без утечек и без time.Sleep вместо синхронизации. Следом обычно просят показать код и спрашивают, как дождаться, что горутина действительно завершилась.
Как отвечать
Цикл обновления строят на time.Ticker и select, а выход привязывают к отмене контекста:
func refresh(ctx context.Context, every time.Duration) error {
t := time.NewTicker(every)
defer t.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-t.C:
if err := reload(ctx); err != nil {
log.Printf("reload: %v", err)
}
}
}
}Ключевые моменты:
- контекст внутрь обновления:
reload(ctx)передаёт его в HTTP-клиент и драйвер БД, иначе отмена ждёт конца долгого запроса; - без
default: веткаdefaultсоSleepпревращает цикл в опрос, и на отмену горутина реагирует только после сна; - ожидание завершения: запуск через
errgroup.Groupилиsync.WaitGroupиWait()перед выходом, а неtime.Sleep(time.Second); - graceful shutdown: корневой контекст из
signal.NotifyContextпоSIGINTиSIGTERM, тогда остановка по сигналу приходит в цикл сама.
Отдельный канал stop рядом с контекстом не нужен: два механизма отмены для одного цикла только запутывают.
Где спотыкаются
Пишут break внутри select и ждут выхода из for. Он прерывает только select, нужен return или break с меткой.
Закрывают канал остановки из нескольких мест. Повторный close вызывает панику, поэтому для отмены удобнее context.CancelFunc: её можно вызывать сколько угодно раз.
Не обрабатывают ошибку обновления, и одна неудачная загрузка то роняет горутину, то молча оставляет устаревший кэш навсегда. Нужны лог, метрика возраста кэша и повтор на следующем тике.