Menu

HTTP-клиент на Golang: GET, POST JSON, таймауты и заголовки

Как делать HTTP-запросы в Go через net/http: http.Get, чтение и закрытие тела, проверка кодов статуса, таймауты http.Client, запросы с контекстом и заголовками, параметры строки запроса и POST с JSON.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

GET-запрос

Каждый пример на этой странице запускает свой сервер через httptest.NewServer и обращается к нему через настоящее TCP-соединение на localhost. Замените srv.URL на настоящий адрес вроде https://api.example.com, и клиентский код не изменится.

Три правила, которые здесь видны:

  1. Сначала проверяйте err. Он сообщает о том, что не удалось получить вообще никакого ответа: ошибки DNS, отказ в соединении, таймауты, проблемы с TLS.
  2. defer resp.Body.Close() сразу после проверки ошибки. Тело держит соединение открытым. Незакрытое тело приводит к утечке соединения.
  3. Читайте или декодируйте тело. io.ReadAll для текста, json.NewDecoder(resp.Body).Decode(&v) для JSON.

Коды статуса не ошибки

http.Get возвращает err == nil для любого ответа, который прислал сервер, включая 404 и 500. Статус нужно проверять самому:

io.LimitReader ограничивает, сколько тела ошибки вы прочитаете, чтобы неисправный сервер не заставил вас загрузить мегабайты в сообщение об ошибке. Многие API возвращают при успехе 201, 202 или 204; если подходит любой 2xx, проверяйте resp.StatusCode >= 200 && resp.StatusCode < 300.

Таймауты: не используйте клиент по умолчанию для реального трафика

У http.Get, http.Post и http.DefaultClient нет общего таймаута (транспорт по умолчанию ограничивает только установку соединения и TLS-рукопожатие). Сервер, который принял соединение и так и не ответил, блокирует вашу горутину навсегда. Создайте свой клиент:

Client.Timeout покрывает весь обмен: соединение, редиректы и чтение тела. Возвращаемая ошибка это *url.Error, у которой Timeout() == true, и errors.As находит её через интерфейс net.Error. Создайте один клиент и используйте его везде; http.Client безопасен для конкурентного использования. Пул соединений живёт в его Transport, а не в Client: новый http.Client{} на каждый запрос всё равно делит http.DefaultTransport и его пул, а вот новый http.Transport на каждый запрос каждый раз начинает с пустым пулом.

Запросы с контекстом и заголовками

http.NewRequestWithContext создаёт запрос, который можно настроить перед отправкой через client.Do. Контекст даёт дедлайн или отмену для отдельного запроса поверх таймаута клиента и привязывает исходящие вызовы к времени жизни входящего запроса на сервере.

Если склеивать пользовательский ввод с URL ("?q=" + q), всё ломается на &, # и пробелах. url.Values и его метод Encode делают экранирование и сортируют ключи. Для сегментов пути используйте url.PathEscape.

POST с JSON

client.Post(url, contentType, body) это сокращение для запроса с таким заголовком Content-Type. Для всего остального (заголовок аутентификации, PUT, PATCH, DELETE) используйте http.NewRequestWithContext с нужным методом и client.Do. Для отправки форм client.PostForm(url, url.Values{...}) кодирует значения и задаёт application/x-www-form-urlencoded.

Повторное использование соединений

Клиент переиспользует TCP-соединение, только когда вы дочитали предыдущее тело до конца и закрыли его. Если вы перестали читать раньше (например, после статуса с ошибкой), а запросов к одному хосту много, вычитайте тело перед закрытием:

defer func() {
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
}()

Для тел, которые могут быть огромными, вычитывайте через io.LimitReader или смиритесь с потерей соединения.

Редиректы

Клиент автоматически следует до 10 редиректов, а resp.Request.URL говорит, где вы в итоге оказались. Чтобы остановиться на первом редиректе, например чтобы прочитать заголовок Location, задайте CheckRedirect:

client := &http.Client{
	Timeout: 10 * time.Second,
	CheckRedirect: func(req *http.Request, via []*http.Request) error {
		return http.ErrUseLastResponse // return the 3xx response itself
	},
}

Частые ошибки

  • http.Get или http.DefaultClient в продакшене. Нет таймаута. Сделайте http.Client с Timeout.
  • Забытый resp.Body.Close(). Соединения утекают, пока запросы не начнут падать.
  • Закрытие тела до проверки err. Когда err не nil, resp равен nil, и resp.Body.Close() вызывает панику.
  • err == nil как признак успеха. Проверяйте resp.StatusCode.
  • Новый http.Transport на каждый запрос. У каждого транспорта свой пул соединений, поэтому каждый запрос открывает новое соединение (и заново проходит TLS-рукопожатие). Делите один транспорт, обычно через один общий http.Client.
  • Ручная сборка строки запроса. Используйте url.Values.

Часто задаваемые вопросы

Как сделать GET-запрос в Go?

resp, err := http.Get(url), проверьте err, затем defer resp.Body.Close() и прочитайте тело через io.ReadAll(resp.Body) или декодируйте его через json.NewDecoder(resp.Body).Decode(&v). Проверяйте и resp.StatusCode: 404 или 500 для http.Get не ошибка.

Как задать таймаут HTTP-запроса в Go?

Создайте клиент с таймаутом, client := &http.Client{Timeout: 10 * time.Second}, и используйте его вместо http.Get или http.DefaultClient, у которых нет общего таймаута: сервер, который принял соединение и так и не ответил, блокирует вызов навсегда. Для ограничения на отдельный запрос создайте его через http.NewRequestWithContext с контекстом из context.WithTimeout.

Как отправить POST-запрос с телом JSON в Go?

Закодируйте значение и отправьте его: body, _ := json.Marshal(v) и http.Post(url, "application/json", bytes.NewReader(body)). Чтобы добавить заголовки вроде Authorization, создайте запрос через http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)), задайте req.Header.Set("Content-Type", "application/json") и отправьте через client.Do(req).

Зачем закрывать resp.Body в Go?

Тело это поток на открытом соединении. Закрытие возвращает соединение в пул для повторного использования; если забыть, соединения и файловые дескрипторы утекают, пока программа не упрётся в лимит. Закрывайте тело, даже если не читаете его, и дочитывайте до конца, если хотите, чтобы соединение переиспользовалось.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ