GinSeniorSystem design

Как настроить таймауты для обработчиков Gin?

Настраивайте ReadTimeout/WriteTimeout на уровне http.Server; для per-handler таймаутов используйте context.WithTimeout, передавая его в c.Request.Context(); для принудительного прерывания хендлера — middleware с goroutine и select.

Таймауты для обработчиков Gin

Gin не имеет встроенного механизма таймаутов для хендлеров. Таймауты настраиваются на нескольких уровнях: TCP-соединение, HTTP-запрос и конкретный хендлер. Каждый уровень решает разные проблемы.

Уровень 1: Таймауты http.Server

Это базовая защита от медленных клиентов и зависших соединений:

srv := &http.Server{
	Addr:         ":8080",
	Handler:      router,
	ReadTimeout:  5 * time.Second,  // время на чтение всего запроса (заголовки + тело)
	WriteTimeout: 10 * time.Second, // время на запись ответа
	IdleTimeout:  120 * time.Second, // keep-alive соединения
	ReadHeaderTimeout: 2 * time.Second, // только заголовки
}

Важно: WriteTimeout отсчитывается от конца чтения запроса до конца записи ответа — он ограничивает всё время выполнения хендлера.

Уровень 2: context.WithTimeout в хендлере

Для ограничения времени конкретной операции (DB-запрос, HTTP-вызов) используйте контекст с дедлайном:

func getProductHandler(c *gin.Context) {
	// Даём 3 секунды на весь хендлер
	ctx, cancel := context.WithTimeout(c.Request.Context(), 3*time.Second)
	defer cancel()

	// Передаём контекст в DB-запрос
	var product Product
	err := db.WithContext(ctx).First(&product, c.Param("id")).Error
	if err != nil {
		if errors.Is(err, context.DeadlineExceeded) {
			c.JSON(http.StatusGatewayTimeout, gin.H{"error": "database timeout"})
			return
		}
		c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusOK, product)
}

Уровень 3: Middleware для принудительного таймаута хендлера

Если хендлер не использует контекст (legacy код), можно принудительно прервать его через middleware с горутиной:

func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
	return func(c *gin.Context) {
		// Создаём контекст с таймаутом
		ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
		defer cancel()

		// Подменяем контекст запроса
		c.Request = c.Request.WithContext(ctx)

		// Канал для сигнала завершения хендлера
		finished := make(chan struct{}, 1)

		go func() {
			c.Next()
			finished <- struct{}{}
		}()

		select {
		case <-finished:
			// Хендлер завершился вовремя
		case <-ctx.Done():
			// Таймаут истёк
			c.AbortWithStatusJSON(http.StatusGatewayTimeout, gin.H{
				"error": "request timeout",
			})
		}
	}
}

// Регистрация:
router.Use(TimeoutMiddleware(5 * time.Second))

Таймауты для исходящих HTTP-запросов

Не забывайте про таймауты при вызове внешних сервисов — дефолтный http.Client не имеет таймаута:

httpClient := &http.Client{
	Timeout: 5 * time.Second,
	Transport: &http.Transport{
		DialContext: (&net.Dialer{
			Timeout:   2 * time.Second, // TCP connect
			KeepAlive: 30 * time.Second,
		}).DialContext,
		TLSHandshakeTimeout:   3 * time.Second,
		ResponseHeaderTimeout: 3 * time.Second,
	},
}

Подводные камни

  • WriteTimeout меньше времени выполнения хендлера — если хендлер работает дольше WriteTimeout, соединение будет разорвано, но хендлер продолжит выполнение в фоне; используйте контекст для ранней отмены.
  • Middleware с горутиной — race condition — горутина хендлера может продолжить писать в c.Writer после того, как middleware написал 504; используйте c.Abort() и проверяйте c.IsAborted().
  • context.WithTimeout не прерывает блокирующие системные вызовы — если хендлер делает блокирующий вызов без контекста (например, time.Sleep), отмена контекста его не остановит.
  • Утечка горутин при таймауте — горутина хендлера продолжает работать после таймаута; убедитесь, что все операции принимают контекст и корректно на него реагируют.
  • ReadTimeout слишком мал для загрузки файловReadTimeout включает время чтения тела; для эндпоинтов загрузки файлов используйте отдельный http.Server с большим таймаутом или сбрасывайте дедлайн через conn.SetDeadline.
  • Не передан контекст в database driver — таймаут сработает в вашем коде, но запрос к БД продолжит выполняться; всегда передавайте контекст через db.WithContext(ctx).
  • IdleTimeout vs ReadTimeoutIdleTimeout относится к keep-alive соединениям между запросами; без него зависшие keep-alive соединения накапливаются и исчерпывают файловые дескрипторы.

Common mistakes

  • Давать ответ про таймауты в Gin только на уровне определения, не показывая поведение в реальном приложении.
  • Игнорировать границы ответственности вокруг темы «таймауты в Gin»: кто отменяет работу, кто владеет ресурсом и где формируется ответ клиенту.
  • Не связывать таймауты в Gin с observability, тестированием или безопасностью, когда это влияет на продакшен-поведение.

What the interviewer is testing

  • Точно объясняет, что именно делает таймауты в Gin и где это используется в Go-коде.
  • Связывает таймауты в Gin с корректным lifecycle запроса, отменой, конкурентностью или конфигурацией сервера там, где это уместно.
  • Не изобретает API и опирается на реальные контракты официальной документации.

Sources

Related topics