GET-запрос
Каждый пример на этой странице запускает свой сервер через httptest.NewServer и обращается к нему через настоящее TCP-соединение на localhost. Замените srv.URL на настоящий адрес вроде https://api.example.com, и клиентский код не изменится.
Три правила, которые здесь видны:
- Сначала проверяйте
err. Он сообщает о том, что не удалось получить вообще никакого ответа: ошибки DNS, отказ в соединении, таймауты, проблемы с TLS. defer resp.Body.Close()сразу после проверки ошибки. Тело держит соединение открытым. Незакрытое тело приводит к утечке соединения.- Читайте или декодируйте тело.
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?
Тело это поток на открытом соединении. Закрытие возвращает соединение в пул для повторного использования; если забыть, соединения и файловые дескрипторы утекают, пока программа не упрётся в лимит. Закрывайте тело, даже если не читаете его, и дочитывайте до конца, если хотите, чтобы соединение переиспользовалось.