Код-ревью Middle Горутины и планировщик GMP обновлено
Код-ревью Go: утечка горутин и тикеров
Вопрос
Обработчик запускает фоновую проверку доступности адреса. Что не так?
func StartHeartbeat(addr string) {
ticker := time.NewTicker(time.Second)
go func() {
for range ticker.C {
if err := ping(addr); err != nil {
log.Println(err)
}
}
}()
}
func handler(w http.ResponseWriter, r *http.Request) {
StartHeartbeat(r.URL.Query().Get("addr"))
w.Write([]byte("ok"))
}
Коротко
Фоновую задачу запускают один раз и привязывают к контексту приложения: В main: StartHeartbeat(appCtx, cfg.HeartbeatAddr), где appCtx отменяется при graceful shutdown. Адрес берётся из конфигурации, а не из запроса.
Что должен найти ревьюер
- Ошибкастрока 3Горутина бесконечна и не имеет способа остановиться (нет ctx или канала stop): каждая такая горутина живёт до конца процесса.
- Производительностьстрока 2Тикер никогда не останавливается (
defer ticker.Stop()), он продолжает тикать вместе с горутиной. - Ошибкастрока 13Новый heartbeat на каждый запрос: через сутки трафика это сотни тысяч горутин и пингов в секунду. Фоновые задачи запускают один раз при старте.
- Безопасностьстрока 13Адрес для пинга берётся из запроса пользователя: это SSRF, сервер начнёт ходить во внутреннюю сеть по чужой команде. Нужен список разрешённых адресов.
Разбор
Фоновую задачу запускают один раз и привязывают к контексту приложения:
func StartHeartbeat(ctx context.Context, addr string) {
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if err := ping(ctx, addr); err != nil {
slog.Warn("heartbeat", "addr", addr, "err", err)
}
}
}
}()
}
В main: StartHeartbeat(appCtx, cfg.HeartbeatAddr), где appCtx отменяется при graceful shutdown. Адрес берётся из конфигурации, а не из запроса.
Ответьте сами, а не прочитайте
В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.