Avatar ·

Как установить таймаут для http.Get() в Golang?

Всем привет! Я новичок в Go и столкнулся с проблемой при выполнении HTTP-запросов. Мне нужно отправлять GET-запросы к внешнему API, но иногда сервер зависает или отвечает очень долго. В стандартном примере из документации я использую `http.Get()`, и он просто висит бесконечно, блокируя всю программу. Вот мой код: ```go resp, err := http.Get("https://api.example.com/data") if err != nil { log.Fatal(err) } defer resp.Body.Close() ``` Проблема в том, что если сервер не отвечает, программа зависает навсегда. Я хочу установить таймаут, например, 5 секунд, чтобы в случае отсутствия ответа программа выдавала ошибку и продолжала работу. Я пробовал искать в документации, но не нашел простого способа добавить таймаут к `http.Get()`. Видел что-то про `http.Client`, но не понял, как это правильно применить. Подскажите, как правильно реализовать таймаут для HTTP-запросов в Go? Нужно ли создавать отдельный клиент или есть способ передать таймаут прямо в `Get()`? Также интересует, как обрабатывать ошибки таймаута, чтобы программа не падала, а логировала проблему и шла дальше. Заранее спасибо за помощь! <<>>

Правильный способ: используйте http.Client с Timeout

В Go стандартная функция http.Get() использует глобальный клиент http.DefaultClient, у которого таймаут по умолчанию не установлен (равен 0, что означает "без таймаута"). Чтобы добавить таймаут, нужно создать собственный экземпляр http.Client и задать поле Timeout.

Вот минимальный рабочий пример:

package main

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

func main() {
    // Создаём клиент с таймаутом 5 секунд
    client := &http.Client{
        Timeout: 5 * time.Second,
    }

    // Выполняем запрос через наш клиент
    resp, err := client.Get("https://api.example.com/data")
    if err != nil {
        // Проверяем, является ли ошибка таймаутом
        if isTimeoutError(err) {
            fmt.Println("Ошибка: превышен таймаут запроса")
        } else {
            fmt.Printf("Ошибка при выполнении запроса: %v\n", err)
        }
        return
    }
    defer resp.Body.Close()

    // Читаем тело ответа
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Ошибка чтения ответа: %v\n", err)
        return
    }

    fmt.Printf("Статус: %s\n", resp.Status)
    fmt.Printf("Тело ответа: %s\n", string(body))
}

// Вспомогательная функция для проверки ошибок таймаута
func isTimeoutError(err error) bool {
    type timeout interface {
        Timeout() bool
    }
    t, ok := err.(timeout)
    return ok && t.Timeout()
}

Почему это работает?

Поле Timeout в http.Client включает в себя время на:

  • Установку соединения (TCP handshake)
  • TLS handshake (если используется HTTPS)
  • Отправку запроса
  • Чтение заголовков ответа
  • Чтение тела ответа (включая все паузы между пакетами)

То есть это полный таймаут на весь HTTP-цикл. Если сервер не отвечает в течение указанного времени, клиент возвращает ошибку.

Более тонкая настройка таймаутов

Если вам нужно разделить таймауты на разные фазы (например, соединение должно установиться за 2 секунды, а чтение данных — за 3 секунды), используйте http.Transport:

client := &http.Client{
    Transport: &http.Transport{
        DialContext: (&net.Dialer{
            Timeout:   2 * time.Second,   // таймаут на установку соединения
            KeepAlive: 30 * time.Second,
        }).DialContext,
        TLSHandshakeTimeout:   2 * time.Second,   // таймаут на TLS
        ResponseHeaderTimeout: 3 * time.Second,   // таймаут на получение заголовков
        ExpectContinueTimeout: 1 * time.Second,
    },
    Timeout: 10 * time.Second, // общий таймаут (перекрывает частные)
}

Обработка ошибок таймаута

Ошибки таймаута в Go реализуют интерфейс net.Error с методом Timeout() bool. Вы можете проверять это разными способами:

// Способ 1: приведение типа
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
    fmt.Println("Таймаут!")
}

// Способ 2: использование errors.As (Go 1.13+)
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
    fmt.Println("Таймаут!")
}

// Способ 3: проверка на конкретные ошибки
if errors.Is(err, context.DeadlineExceeded) {
    fmt.Println("Таймаут через контекст!")
}

Важное замечание

Не используйте http.Get() в production-коде без явного указания таймаута. Всегда создавайте собственный клиент с осмысленными значениями таймаутов. Это предотвратит зависание программы и утечку горутин.

<<>>

Альтернативный подход: использование контекста (context.Context)

Другой способ установить таймаут — использовать context.WithTimeout вместе с http.NewRequestWithContext(). Этот метод даёт больше гибкости, например, возможность отменять запрос из нескольких мест одновременно.

package main

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

func main() {
    // Создаём контекст с таймаутом 5 секунд
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel() // обязательно вызываем cancel для освобождения ресурсов

    // Создаём запрос с контекстом
    req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
    if err != nil {
        fmt.Printf("Ошибка создания запроса: %v\n", err)
        return
    }

    // Выполняем запрос через стандартный клиент (или свой)
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        // Проверяем, не из-за таймаута ли ошибка
        if ctx.Err() == context.DeadlineExceeded {
            fmt.Println("Запрос превысил таймаут")
        } else {
            fmt.Printf("Ошибка выполнения запроса: %v\n", err)
        }
        return
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}

Плюсы этого подхода:

  • Контекст можно передавать вниз по цепочке вызовов (например, в базу данных или другие HTTP-запросы)
  • Легко комбинировать таймаут с отменой по сигналу (Ctrl+C) через signal.NotifyContext
  • Более идиоматичный Go-стиль для сложных приложений

Минусы: требует больше кода, чем просто client.Timeout. Для простых скриптов или однократных запросов лучше использовать первый подход с http.Client.