Menu

Klient HTTP w Golang: GET, POST JSON, timeouty i nagłówki

Jak wysyłać żądania HTTP w Go z net/http: http.Get, czytanie i zamykanie body, sprawdzanie kodów statusu, timeouty w http.Client, żądania z kontekstem i nagłówkami, parametry zapytania i wysyłanie JSON metodą POST.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Żądanie GET

Każdy przykład na tej stronie uruchamia własny serwer przez httptest.NewServer i łączy się z nim prawdziwym połączeniem TCP na localhost. Zamień srv.URL na prawdziwy adres (np. https://api.example.com), a kod klienta pozostanie bez zmian.

Widać tu trzy zasady:

  1. Najpierw sprawdź err. Zgłasza sytuacje, w których nie udało się dostać żadnej odpowiedzi: błędy DNS, odrzucone połączenia, timeouty, problemy z TLS.
  2. defer resp.Body.Close() zaraz po sprawdzeniu błędu. Body trzyma otwarte połączenie. Niezamknięte body powoduje wyciek połączenia.
  3. Przeczytaj lub zdekoduj body. io.ReadAll dla tekstu, json.NewDecoder(resp.Body).Decode(&v) dla JSON.

Kody statusu to nie błędy

http.Get zwraca err == nil dla każdej odpowiedzi, którą wyśle serwer, także 404 i 500. Status musisz sprawdzić sam:

io.LimitReader ogranicza, ile body błędu przeczytasz, więc źle działający serwer nie zmusi cię do wczytania megabajtów do komunikatu błędu. Wiele API zwraca przy sukcesie 201, 202 albo 204; sprawdzaj resp.StatusCode >= 200 && resp.StatusCode < 300, gdy każdy kod 2xx jest w porządku.

Timeouty: nigdy nie używaj domyślnego klienta do prawdziwego ruchu

http.Get, http.Post i http.DefaultClient nie mają ogólnego timeoutu (domyślny transport ogranicza tylko nawiązywanie połączenia i handshake TLS). Serwer, który przyjmie połączenie i nigdy nie odpowie, zablokuje twoją gorutynę na zawsze. Utwórz własnego klienta:

Client.Timeout obejmuje całą wymianę: łączenie, przekierowania i czytanie body. Zwracany błąd to *url.Error, który zgłasza Timeout() == true, a errors.As znajduje go przez interfejs net.Error. Utwórz jednego klienta i używaj go wszędzie; http.Client jest bezpieczny przy współbieżnym użyciu. Pula połączeń żyje w jego Transport, a nie w Client: nowy http.Client{} na każde żądanie nadal współdzieli http.DefaultTransport i jego pulę, ale nowy http.Transport na każde żądanie za każdym razem zaczyna z pustą pulą.

Żądania z kontekstem i nagłówkami

http.NewRequestWithContext buduje żądanie, które możesz dostosować przed wysłaniem przez client.Do. Kontekst daje termin lub anulowanie dla pojedynczego żądania, niezależnie od timeoutu klienta, i wiąże wywołania wychodzące z czasem życia przychodzącego żądania na serwerze.

Doklejanie danych od użytkownika do URL ("?q=" + q) psuje się na &, # i spacjach. url.Values i jego metoda Encode zajmują się escapowaniem i sortują klucze. Dla segmentów ścieżki użyj url.PathEscape.

POST JSON

client.Post(url, contentType, body) to skrót dla żądania z takim nagłówkiem Content-Type. Do wszystkiego innego (nagłówek autoryzacji, PUT, PATCH, DELETE) użyj http.NewRequestWithContext z metodą i client.Do. Przy wysyłaniu formularzy client.PostForm(url, url.Values{...}) koduje wartości i ustawia application/x-www-form-urlencoded.

Ponowne użycie połączeń

Klient ponownie używa połączenia TCP tylko wtedy, gdy przeczytasz poprzednie body do końca i je zamkniesz. Jeśli przerwiesz czytanie wcześniej (np. po statusie błędu), a wysyłasz wiele żądań do tego samego hosta, opróżnij body przed zamknięciem:

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

Przy body, które może być ogromne, opróżniaj je przez io.LimitReader albo pogódź się z utratą połączenia.

Przekierowania

Klient automatycznie wykonuje do 10 przekierowań, a resp.Request.URL mówi, pod jakim adresem kończy się ten łańcuch. Aby zatrzymać się na pierwszym przekierowaniu, np. żeby odczytać nagłówek Location, ustaw 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
	},
}

Częste błędy

  • Używanie http.Get lub http.DefaultClient na produkcji. Brak timeoutu. Utwórz http.Client z Timeout.
  • Brak resp.Body.Close(). Połączenia wyciekają, aż żądania zaczynają zawodzić.
  • Zamykanie body przed sprawdzeniem err. Gdy err nie jest nil, resp jest nil, a resp.Body.Close() wywołuje panikę.
  • Traktowanie err == nil jako sukcesu. Sprawdzaj resp.StatusCode.
  • Tworzenie nowego http.Transport na każde żądanie. Każdy transport ma własną pulę połączeń, więc każde żądanie otwiera nowe połączenie (i robi nowy handshake TLS). Współdziel jeden transport, zwykle przez jeden wspólny http.Client.
  • Ręczne budowanie query stringów. Użyj url.Values.

Najczęściej zadawane pytania

Jak wysłać żądanie GET w Go?

resp, err := http.Get(url), sprawdź err, potem defer resp.Body.Close() i przeczytaj body przez io.ReadAll(resp.Body) albo zdekoduj je przez json.NewDecoder(resp.Body).Decode(&v). Sprawdź też resp.StatusCode: 404 czy 500 to nie jest błąd zwracany przez http.Get.

Jak ustawić timeout żądania HTTP w Go?

Utwórz klienta z timeoutem, client := &http.Client{Timeout: 10 * time.Second}, i używaj go zamiast http.Get czy http.DefaultClient, które nie mają ogólnego timeoutu: serwer, który przyjmie połączenie i nigdy nie odpowie, zablokuje wywołanie na zawsze. Dla limitu na pojedyncze żądanie zbuduj je przez http.NewRequestWithContext z kontekstem z context.WithTimeout.

Jak wysłać żądanie POST z body w JSON w Go?

Zserializuj wartość, a potem ją wyślij: body, _ := json.Marshal(v) i http.Post(url, "application/json", bytes.NewReader(body)). Aby dodać nagłówki, np. Authorization, zbuduj żądanie przez http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)), ustaw req.Header.Set("Content-Type", "application/json") i wyślij je przez client.Do(req).

Dlaczego w Go trzeba zamykać resp.Body?

Body to strumień na otwartym połączeniu. Zamknięcie go oddaje połączenie do puli do ponownego użycia; bez zamknięcia połączenia i deskryptory plików wyciekają, aż program osiągnie limit. Zamykaj body nawet wtedy, gdy go nie czytasz, i czytaj je do końca, jeśli połączenie ma zostać użyte ponownie.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ