• English
  • Example: HTTP GET

    Use HTTP GET to retrieve a resource from an HTTP server.

    A GET request normally reads a resource without modifying server-side state.

    Quick Example

    package main
    
    import (
    	"fmt"
    	"io"
    	"net/http"
    )
    
    func main() {
    	resp, err := http.Get("https://example.com")
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    	defer resp.Body.Close()
    
    	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    		// Drain a small amount to help connection reuse for small responses.
    		_, _ = io.CopyN(io.Discard, resp.Body, 4<<10)
    		fmt.Println("unexpected status:", resp.Status)
    		return
    	}
    
    	body, err := io.ReadAll(resp.Body)
    	if err != nil {
    		fmt.Println(err)
    		return
    	}
    
    	fmt.Print(string(body))
    }

    This is a minimal example.

    Production code should normally return errors rather than printing them.

    The basic flow is:

    HTTP request
    
    transport error?
    
    check HTTP status
    
    close response body
    
    read the body

    The important distinction is that err == nil does not mean the HTTP request succeeded at the application level.

    Use Cases

    • Fetch JSON from an HTTP API.
    • Retrieve a small resource.
    • Call a read-only HTTP endpoint.

    For simple requests, http.Get is convenient.

    For production code, an explicit http.Client is usually a better starting point.

    Use an Explicit Timeout

    http.Get uses http.DefaultClient.

    The default client's Timeout is zero, meaning no overall timeout is set.

    A remote server can therefore leave a request waiting indefinitely.

    client := &http.Client{
    	Timeout: 10 * time.Second,
    }
    
    resp, err := client.Get("https://example.com")
    if err != nil {
    	return err
    }
    defer resp.Body.Close()

    For request-specific cancellation, use a context:

    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
    	return err
    }
    
    resp, err := client.Do(req)
    if err != nil {
    	return err
    }
    defer resp.Body.Close()

    These examples show timeout and cancellation only.

    A production implementation should also check StatusCode, bound the response body when its size is not trusted, and handle the response body lifecycle correctly.

    http.Client.Timeout is an overall request timeout. It continues to apply while the response body is being read.

    This matters for a server that sends headers quickly but delivers the body very slowly.

    err == nil Is Not a 2xx Response

    A response such as:

    404 Not Found
    500 Internal Server Error

    is still a successful HTTP exchange from the transport's perspective.

    The client may return:

    resp, err := client.Get(url)
    
    if err != nil {
    	return err
    }

    with:

    err == nil

    while:

    resp.StatusCode == 500

    Therefore, check the status separately:

    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    	return fmt.Errorf("unexpected status: %s", resp.Status)
    }

    Think of the two checks as different layers:

    err
    └── Could I communicate with the server?
    
    StatusCode
    └── What did the server say?

    Always Close resp.Body

    A successful request returns a response body that your code owns.

    Close it:

    resp, err := client.Get(url)
    if err != nil {
    	return err
    }
    defer resp.Body.Close()

    Put the defer immediately after checking err.

    http.Get does not close the response body for you.

    The body should also normally be read to EOF when practical. If the body is closed before being fully consumed, the underlying connection may not be reusable for a subsequent request.

    For an error response that you do not need to inspect, draining a small amount before closing can help when the response is small:

    // Drain up to 4 KiB to help connection reuse.
    _, _ = io.CopyN(io.Discard, resp.Body, 4<<10)

    This is only a best effort. It does not guarantee connection reuse if the response body is larger than the drained amount.

    Don't Blindly io.ReadAll

    This is convenient:

    body, err := io.ReadAll(resp.Body)

    It is also an unbounded memory operation.

    For a small, trusted response, that may be exactly what you want.

    For an untrusted or potentially large response, impose a limit.

    A subtle mistake is:

    body, err := io.ReadAll(
    	io.LimitReader(resp.Body, maxSize),
    )

    This limits what is read, but it cannot tell whether:

    • the response really ended, or
    • LimitReader reached its artificial limit.

    If exceeding the limit must be detected, read one extra byte:

    const maxSize = 1 << 20 // 1 MiB
    
    body, err := io.ReadAll(
    	io.LimitReader(resp.Body, maxSize+1),
    )
    if err != nil {
    	return err
    }
    
    if len(body) > maxSize {
    	return fmt.Errorf("response body too large")
    }

    This is an important interaction between net/http and io: the artificial EOF produced by LimitReader is indistinguishable from a real end of input to io.ReadAll.

    ContentLength Is Not a Complete Size Check

    ContentLength can reject an obviously oversized response before reading it:

    if resp.ContentLength > maxSize {
    	return fmt.Errorf("response body too large: %d bytes", resp.ContentLength)
    }

    But:

    resp.ContentLength == -1

    means the response length is unknown.

    The body may be streamed, so ContentLength cannot replace a bounded read.

    In practice, use both:

    const maxSize = 1 << 20 // 1 MiB
    
    if resp.ContentLength > maxSize {
    	return fmt.Errorf("response body too large: %d bytes", resp.ContentLength)
    }
    
    body, err := io.ReadAll(
    	io.LimitReader(resp.Body, maxSize+1),
    )
    if err != nil {
    	return err
    }
    
    if len(body) > maxSize {
    	return fmt.Errorf("response body too large")
    }

    ContentLength provides an early rejection when the size is known.

    LimitReader provides the actual memory bound when the size is unknown.

    Retries Are a Separate Policy

    GET is generally safe to retry because it is intended to be idempotent.

    That does not mean every failed GET should be retried.

    Typical candidates include:

    • temporary network failures
    • 429 Too Many Requests
    • 502 Bad Gateway
    • 503 Service Unavailable
    • 504 Gateway Timeout

    A 429 response may also provide Retry-After, which can be used by the retry policy.

    Most ordinary 4xx responses, such as 400, 401, 403, and 404, should not be retried automatically.

    Retries should also respect the original request context and deadline.

    Otherwise, each attempt can effectively create another full timeout window and turn a bounded operation into an unexpectedly long one.

    Retry policy belongs above http.Client; http.Get does not provide it.

    Redirects Are Followed by Default

    Go's http.Client follows redirects by default.

    The default policy follows up to 10 consecutive redirects.

    For example:

    GET /old
    
    301 /new
    
    GET /new

    A redirect chain can consume the request's entire timeout budget before the final response arrives.

    Redirects can also cross trust boundaries.

    If the request contains sensitive headers such as Authorization, review the redirect behavior instead of assuming those headers are sent unchanged to every destination.

    Go's current net/http implementation does protect sensitive headers when redirecting to an unrelated domain, but redirect behavior is still an application policy decision.

    Use CheckRedirect when the application needs explicit control:

    client := &http.Client{
    	CheckRedirect: func(req *http.Request, via []*http.Request) error {
    		return http.ErrUseLastResponse
    	},
    }

    This disables following the redirect and returns the most recent response.

    There is no http.NoCheckRedirect variable in the Go standard library; CheckRedirect and http.ErrUseLastResponse are the standard mechanisms.

    A Production-Oriented GET

    The following combines the main concerns discussed above for a small response whose maximum size is known:

    func fetch(ctx context.Context, client *http.Client, url string) ([]byte, error) {
    	const maxSize = 1 << 20 // 1 MiB
    
    	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    	if err != nil {
    		return nil, err
    	}
    
    	resp, err := client.Do(req)
    	if err != nil {
    		return nil, err
    	}
    	defer resp.Body.Close()
    
    	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
    		// Drain a small amount to help connection reuse for small responses.
    		_, _ = io.CopyN(io.Discard, resp.Body, 4<<10)
    		return nil, fmt.Errorf("unexpected status: %s", resp.Status)
    	}
    
    	if resp.ContentLength > maxSize {
    		return nil, fmt.Errorf("response body too large: %d bytes", resp.ContentLength)
    	}
    
    	body, err := io.ReadAll(
    		io.LimitReader(resp.Body, maxSize+1),
    	)
    	if err != nil {
    		return nil, fmt.Errorf("read response body: %w", err)
    	}
    
    	if len(body) > maxSize {
    		return nil, fmt.Errorf("response body too large")
    	}
    
    	return body, nil
    }

    The client itself can be configured once and reused:

    client := &http.Client{
    	Timeout: 10 * time.Second,
    }

    This is not a universal HTTP client template. For large responses, stream the body instead of using io.ReadAll. For retries, authentication, redirects, and protocol-specific status handling, add the policy required by the application.

    Common Mistakes

    1. No timeout

    http.Get(url)

    is fine for a minimal example.

    It is often the wrong default for a service that depends on a remote server.

    Use an explicit client when timeout behavior matters.

    2. Treating HTTP errors as Go errors

    resp, err := client.Get(url)
    
    if err != nil {
    	return err
    }

    does not detect HTTP 404 or 500.

    Check StatusCode separately.

    3. Forgetting Body.Close

    resp, err := client.Get(url)
    if err != nil {
    	return err
    }
    
    body, err := io.ReadAll(resp.Body)

    The response body is still your responsibility.

    4. Reading an unbounded body

    io.ReadAll(resp.Body)

    can allocate according to the response size.

    Use a limit when the response size is not trusted.

    5. Assuming ContentLength is always available

    ContentLength == -1 is valid.

    A missing length does not mean the body is empty.

    6. Adding retries without a deadline

    Retries multiply waiting time.

    Keep retries inside the request's overall time budget.

    7. Assuming redirects are harmless

    Redirects are requests too.

    Review where they can go when the URL or request headers contain security-sensitive information.

    Rule of Thumb

    For a simple GET:

    resp, err := client.Get(url)
    if err != nil {
    	return err
    }
    defer resp.Body.Close()

    Then handle the layers separately:

    • transport failure → check err
    • HTTP failure → check StatusCode
    • resource cleanup → close Body
    • connection reuse → consume the body when practical
    • untrusted response → bound the body
    • known large response → reject with ContentLength early
    • slow server → set a timeout
    • repeated failures → define a retry policy
    • redirects across trust boundaries → define a redirect policy

    http.Get is easy to call.

    The engineering work is deciding what happens around the call.