Таймауты в Go: context на каждый запрос
Сервис, который бесконечно ждёт ответа от базы, рано или поздно соберёт тысячи висящих горутин. Правило, которому я следую: у каждого входящего запроса есть дедлайн, и он передаётся во все вызовы ниже.
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
user, err := h.store.UserByID(ctx, r.PathValue("id"))
switch {
case errors.Is(err, context.DeadlineExceeded):
http.Error(w, "upstream timeout", http.StatusGatewayTimeout)
return
case err != nil:
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
writeJSON(w, user)
}
Что здесь важно
defer cancel()обязателен: иначе таймер и связанные ресурсы живут до истечения таймаута.go vetоб этом предупредит.- Контекст строится от
r.Context(): если клиент оборвал соединение, работа отменится сразу, не дожидаясь двух секунд. - Контекст должен дойти до самого низа:
QueryContext,ExecContext, а в pgx — любые методы сctxпервым аргументом.
И не забыть про таймауты самого сервера и HTTP-клиента — по умолчанию их нет:
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
client := &http.Client{Timeout: 10 * time.Second}
http.DefaultClient без таймаута — частая причина «зависших» сервисов: удалённая сторона перестала отвечать, а мы ждём вечно.