Таймауты в 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 без таймаута — частая причина «зависших» сервисов: удалённая сторона перестала отвечать, а мы ждём вечно.