Код-ревью 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. Адрес берётся из конфигурации, а не из запроса.

Что должен найти ревьюер

  1. Ошибкастрока 3
    Горутина бесконечна и не имеет способа остановиться (нет ctx или канала stop): каждая такая горутина живёт до конца процесса.
  2. Производительностьстрока 2
    Тикер никогда не останавливается (defer ticker.Stop()), он продолжает тикать вместе с горутиной.
  3. Ошибкастрока 13
    Новый heartbeat на каждый запрос: через сутки трафика это сотни тысяч горутин и пингов в секунду. Фоновые задачи запускают один раз при старте.
  4. Безопасностьстрока 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 ошибок.