Как настроить таймауты для обработчиков 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 ReadTimeout —
IdleTimeoutотносится к keep-alive соединениям между запросами; без него зависшие keep-alive соединения накапливаются и исчерпывают файловые дескрипторы.
Common mistakes
- Давать ответ про таймауты в Gin только на уровне определения, не показывая поведение в реальном приложении.
- Игнорировать границы ответственности вокруг темы «таймауты в Gin»: кто отменяет работу, кто владеет ресурсом и где формируется ответ клиенту.
- Не связывать таймауты в Gin с observability, тестированием или безопасностью, когда это влияет на продакшен-поведение.
What the interviewer is testing
- Точно объясняет, что именно делает таймауты в Gin и где это используется в Go-коде.
- Связывает таймауты в Gin с корректным lifecycle запроса, отменой, конкурентностью или конфигурацией сервера там, где это уместно.
- Не изобретает API и опирается на реальные контракты официальной документации.