Go context.WithTimeout: ограничить HTTP-запрос

Категория: Backend · Язык: Go · Обновлено:

context.WithTimeout создаёт контекст, который отменяется через заданный интервал. Его передают в HTTP-запрос, запрос к базе или горутину, чтобы операция не висела бесконечно. Функция возвращает контекст и cancel, который нужно вызывать всегда — иначе таймер и связанные ресурсы живут до истечения срока.

Проверить срабатывание таймаута

Go
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()

    select {
    case <-time.After(200 * time.Millisecond):
        fmt.Println("слишком долго")
    case <-ctx.Done():
        fmt.Println("таймаут:", ctx.Err())
    }
}

Через 50 мс контекст закрывает канал Done(), а ctx.Err() возвращает context deadline exceeded. Если операция завершилась раньше, cancel() освобождает таймер, не дожидаясь дедлайна. Поэтому defer cancel() ставят сразу после создания контекста, даже если ошибка кажется невозможной.

Отправить HTTP-запрос с дедлайном

Go
package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"
)

func fetch(ctx context.Context, url string) (string, error) {
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return "", err
    }
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    return string(body), err
}

func main() {
    body, err := fetch(context.Background(), "https://example.com")
    if err != nil {
        fmt.Println("ошибка:", err)
        return
    }
    fmt.Println(len(body))
}

http.NewRequestWithContext связывает запрос с контекстом: при отмене транспорт прерывает соединение, а Do возвращает ошибку, которую можно проверить через errors.Is(err, context.DeadlineExceeded). В обработчике HTTP используйте r.Context() как родителя, чтобы отмена клиентом тоже останавливала работу.

http.Client.Timeout ограничивает весь запрос, включая чтение тела, но не помогает передать дедлайн в базу. Контекст удобнее для цепочки вызовов: передавайте его первым аргументом и не сохраняйте в структуре надолго. Контекст не прерывает код, который не проверяет Done(); циклы и тяжёлые вычисления должны сами реагировать на отмену.

Не создавайте контекст с таймаутом на весь сервис и не используйте context.Background() внутри обработчика. Для фоновых задач с собственным сроком жизни Background() уместен, но отмену всё равно нужно связать с остановкой приложения.

Документация: context, net/http Request.WithContext.