A GET request
Every example on this page starts its own server with httptest.NewServer and calls it through a real TCP connection on localhost. Swap srv.URL for a real address like https://api.example.com and the client code is unchanged.
The three rules visible here:
- Check
errfirst. It reports failures to get any response at all: DNS errors, refused connections, timeouts, TLS problems. defer resp.Body.Close()right after the error check. The body holds the connection open. An unclosed body leaks the connection.- Read or decode the body.
io.ReadAllfor text,json.NewDecoder(resp.Body).Decode(&v)for JSON.
Status codes are not errors
http.Get returns err == nil for any response the server sends, including 404 and 500. You have to check the status yourself:
io.LimitReader caps how much of an error body you read, so a misbehaving server cannot make you load megabytes into an error message. Many APIs return 201, 202 or 204 for success; check resp.StatusCode >= 200 && resp.StatusCode < 300 when any 2xx is fine.
Timeouts: never use the default client for real traffic
http.Get, http.Post and http.DefaultClient have no overall timeout (the default transport only limits dialing and the TLS handshake). A server that accepts the connection and never answers blocks your goroutine forever. Create your own client:
Client.Timeout covers the whole exchange: connecting, redirects, and reading the body. The error it returns is a *url.Error that reports Timeout() == true, which errors.As finds through the net.Error interface. Create one client and reuse it everywhere; an http.Client is safe for concurrent use. The connection pool lives in its Transport, not in the Client: a new http.Client{} per request still shares http.DefaultTransport and its pool, but a new http.Transport per request starts with an empty pool every time.
Requests with a context and headers
http.NewRequestWithContext builds a request you can customize before sending it with client.Do. The context gives a per-request deadline or cancellation on top of the client's timeout, and ties outgoing calls to the lifetime of an incoming request in a server.
Concatenating user input into a URL ("?q=" + q) breaks on &, # and spaces. url.Values and its Encode method do the escaping, and sort the keys. For path segments, use url.PathEscape.
POST JSON
client.Post(url, contentType, body) is a shortcut for a request with that Content-Type header. For anything else (an auth header, PUT, PATCH, DELETE) use http.NewRequestWithContext with the method and client.Do. For form posts, client.PostForm(url, url.Values{...}) encodes the values and sets application/x-www-form-urlencoded.
Connection reuse
The client reuses a TCP connection only when you read the previous body to the end and close it. If you stop reading early (for example after an error status), drain it before closing when you make many requests to the same host:
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
For bodies that could be huge, drain through io.LimitReader instead, or accept losing the connection.
Redirects
The client follows up to 10 redirects automatically, and resp.Request.URL tells you where you ended up. To stop at the first redirect, for example to read a Location header, set 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
},
}
Common mistakes
- Using
http.Getorhttp.DefaultClientin production. No timeout. Make anhttp.ClientwithTimeout. - Forgetting
resp.Body.Close(). Connections leak until requests start failing. - Closing the body before checking
err. Whenerris notnil,respisnilandresp.Body.Close()panics. - Treating
err == nilas success. Checkresp.StatusCode. - Creating a new
http.Transportper request. Each transport has its own connection pool, so every request opens a fresh connection (and a fresh TLS handshake). Share one transport, usually through one sharedhttp.Client. - Building query strings by hand. Use
url.Values.
Frequently Asked Questions
How do I make a GET request in Go?
resp, err := http.Get(url), check err, then defer resp.Body.Close() and read the body with io.ReadAll(resp.Body) or decode it with json.NewDecoder(resp.Body).Decode(&v). Also check resp.StatusCode: a 404 or 500 is not an error from http.Get.
How do I set a timeout on an HTTP request in Go?
Create a client with a timeout, client := &http.Client{Timeout: 10 * time.Second}, and use it instead of http.Get or http.DefaultClient, which have no overall timeout: a server that accepts the connection and never answers blocks the call forever. For a per-request limit, build the request with http.NewRequestWithContext and a context from context.WithTimeout.
How do I send a POST request with a JSON body in Go?
Marshal the value, then post it: body, _ := json.Marshal(v) and http.Post(url, "application/json", bytes.NewReader(body)). To add headers such as Authorization, build it with http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)), set req.Header.Set("Content-Type", "application/json"), and send it with client.Do(req).
Why do I need to close resp.Body in Go?
The body is a stream on an open connection. Closing it returns the connection to the pool for reuse; forgetting leaks connections and file descriptors until the program hits its limit. Close it even when you do not read it, and read it to the end if you want the connection reused.