Żą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:
- 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. defer resp.Body.Close()zaraz po sprawdzeniu błędu. Body trzyma otwarte połączenie. Niezamknięte body powoduje wyciek połączenia.- Przeczytaj lub zdekoduj body.
io.ReadAlldla 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.Getlubhttp.DefaultClientna produkcji. Brak timeoutu. Utwórzhttp.ClientzTimeout. - Brak
resp.Body.Close(). Połączenia wyciekają, aż żądania zaczynają zawodzić. - Zamykanie body przed sprawdzeniem
err. Gdyerrnie jestnil,respjestnil, aresp.Body.Close()wywołuje panikę. - Traktowanie
err == niljako sukcesu. Sprawdzajresp.StatusCode. - Tworzenie nowego
http.Transportna 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ólnyhttp.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.