Menu

Golang HTTP-Client: GET, POST mit JSON, Timeouts und Header

So stellst du in Go mit net/http HTTP-Requests: http.Get, den Body lesen und schließen, Statuscodes prüfen, Timeouts mit http.Client, Requests mit Context und Headern, Query-Parameter und JSON per POST senden.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

Ein GET-Request

Jedes Beispiel auf dieser Seite startet mit httptest.NewServer einen eigenen Server und ruft ihn über eine echte TCP-Verbindung auf localhost auf. Ersetz srv.URL durch eine echte Adresse wie https://api.example.com, und der Client-Code bleibt unverändert.

Die drei Regeln, die hier zu sehen sind:

  1. Zuerst err prüfen. Es meldet Fehlschläge, bei denen gar keine Antwort ankommt: DNS-Fehler, abgelehnte Verbindungen, Timeouts, TLS-Probleme.
  2. defer resp.Body.Close() direkt nach der Fehlerprüfung. Der Body hält die Verbindung offen. Ein nicht geschlossener Body leakt die Verbindung.
  3. Den Body lesen oder dekodieren. io.ReadAll für Text, json.NewDecoder(resp.Body).Decode(&v) für JSON.

Statuscodes sind keine Fehler

http.Get gibt für jede Antwort, die der Server sendet, err == nil zurück, auch für 404 und 500. Den Status musst du selbst prüfen:

io.LimitReader begrenzt, wie viel von einem Fehler-Body du liest, damit ein fehlerhafter Server dich nicht Megabytes in eine Fehlermeldung laden lässt. Viele APIs geben bei Erfolg 201, 202 oder 204 zurück; prüf resp.StatusCode >= 200 && resp.StatusCode < 300, wenn jeder 2xx-Code in Ordnung ist.

Timeouts: für echten Traffic nie den Standard-Client

http.Get, http.Post und http.DefaultClient haben keinen Gesamt-Timeout (der Standard-Transport begrenzt nur den Verbindungsaufbau und den TLS-Handshake). Ein Server, der die Verbindung annimmt und nie antwortet, blockiert deine Goroutine für immer. Erzeuge deinen eigenen Client:

Client.Timeout deckt den ganzen Austausch ab: Verbindungsaufbau, Redirects und das Lesen des Bodys. Der zurückgegebene Fehler ist ein *url.Error, der Timeout() == true meldet, und errors.As findet ihn über das Interface net.Error. Erzeuge einen Client und verwende ihn überall wieder; ein http.Client ist sicher für nebenläufige Nutzung. Der Verbindungspool lebt in seinem Transport, nicht im Client: Ein neuer http.Client{} pro Request teilt sich weiterhin http.DefaultTransport und dessen Pool, aber ein neuer http.Transport pro Request startet jedes Mal mit einem leeren Pool.

Requests mit Context und Headern

http.NewRequestWithContext baut einen Request, den du anpassen kannst, bevor du ihn mit client.Do sendest. Der Context liefert zusätzlich zum Timeout des Clients eine Deadline oder einen Abbruch pro Request und koppelt ausgehende Aufrufe in einem Server an die Lebensdauer eines eingehenden Requests.

Nutzereingaben in eine URL zu verketten ("?q=" + q) bricht bei &, # und Leerzeichen. url.Values und seine Methode Encode übernehmen das Escaping und sortieren die Schlüssel. Für Pfadsegmente nimm url.PathEscape.

POST mit JSON

client.Post(url, contentType, body) ist eine Abkürzung für einen Request mit diesem Content-Type-Header. Für alles andere (einen Auth-Header, PUT, PATCH, DELETE) nimm http.NewRequestWithContext mit der Methode und client.Do. Für Formulare kodiert client.PostForm(url, url.Values{...}) die Werte und setzt application/x-www-form-urlencoded.

Wiederverwendung von Verbindungen

Der Client verwendet eine TCP-Verbindung nur wieder, wenn du den vorherigen Body bis zum Ende gelesen und geschlossen hast. Hörst du früher auf zu lesen (zum Beispiel nach einem Fehlerstatus), leere ihn vor dem Schließen, wenn du viele Requests an denselben Host stellst:

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

Bei Bodys, die riesig sein könnten, leerst du stattdessen über io.LimitReader oder nimmst den Verlust der Verbindung in Kauf.

Redirects

Der Client folgt automatisch bis zu 10 Redirects, und resp.Request.URL sagt dir, wo du gelandet bist. Um beim ersten Redirect anzuhalten, etwa um einen Location-Header zu lesen, setz 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
	},
}

Häufige Fehler

  • http.Get oder http.DefaultClient in Produktion verwenden. Kein Timeout. Erzeuge einen http.Client mit Timeout.
  • resp.Body.Close() vergessen. Verbindungen leaken, bis Requests fehlschlagen.
  • Den Body schließen, bevor err geprüft ist. Ist err nicht nil, ist resp nil, und resp.Body.Close() löst eine Panic aus.
  • err == nil als Erfolg werten. Prüf resp.StatusCode.
  • Einen neuen http.Transport pro Request erzeugen. Jeder Transport hat seinen eigenen Verbindungspool, also öffnet jeder Request eine neue Verbindung (samt neuem TLS-Handshake). Teile einen Transport, meist über einen gemeinsamen http.Client.
  • Query-Strings von Hand bauen. Nimm url.Values.

Häufig gestellte Fragen

Wie stelle ich in Go einen GET-Request?

resp, err := http.Get(url), err prüfen, dann defer resp.Body.Close() und den Body mit io.ReadAll(resp.Body) lesen oder mit json.NewDecoder(resp.Body).Decode(&v) dekodieren. Prüf außerdem resp.StatusCode: Ein 404 oder 500 ist für http.Get kein Fehler.

Wie setze ich in Go einen Timeout für einen HTTP-Request?

Erzeuge einen Client mit Timeout, client := &http.Client{Timeout: 10 * time.Second}, und nutze ihn statt http.Get oder http.DefaultClient. Die haben keinen Gesamt-Timeout: Ein Server, der die Verbindung annimmt und nie antwortet, blockiert den Aufruf für immer. Für ein Limit pro Request baust du den Request mit http.NewRequestWithContext und einem Context aus context.WithTimeout.

Wie sende ich in Go einen POST-Request mit JSON-Body?

Den Wert marshallen und dann senden: body, _ := json.Marshal(v) und http.Post(url, "application/json", bytes.NewReader(body)). Um Header wie Authorization hinzuzufügen, baust du den Request mit http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)), setzt req.Header.Set("Content-Type", "application/json") und sendest ihn mit client.Do(req).

Warum muss ich in Go resp.Body schließen?

Der Body ist ein Stream auf einer offenen Verbindung. Schließt du ihn, geht die Verbindung zur Wiederverwendung zurück in den Pool; vergisst du es, leaken Verbindungen und File Descriptors, bis das Programm sein Limit erreicht. Schließ ihn auch, wenn du ihn nicht liest, und lies ihn bis zum Ende, wenn die Verbindung wiederverwendet werden soll.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S