Go by Example

HTTP Client

Always set Timeout on http.Client - the zero-value client has no timeout and holds goroutines open indefinitely when a downstream server is slow or unresponsive.

http.Get and http.Post are convenience functions backed by the package-level http.DefaultClient. For production use, create an explicit http.Client with a timeout so slow servers cannot hold goroutines open forever.

http.Get makes a GET request and returns a *http.Response. Always read the full body and close it - even if you discard the content - to allow connection reuse by the underlying transport.

package main
 
import (
    "fmt"
    "io"
    "net/http"
)
 
func main() {
    resp, err := http.Get("https://httpbin.org/get")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
 
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    fmt.Printf("status: %s\n", resp.Status)
    fmt.Printf("body: %s\n", body)
}

http.NewRequestWithContext creates a request tied to a context. Pass the context from the caller so that timeouts and cancellations propagate correctly through the full call chain.

package main
 
import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"
)
 
func main() {
    client := &http.Client{Timeout: 5 * time.Second}
 
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
 
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://httpbin.org/delay/1", nil)
    if err != nil {
        panic(err)
    }
    req.Header.Set("Accept", "application/json")
    req.Header.Set("Authorization", "Bearer my-token")
 
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("request failed:", err)
        return
    }
    defer resp.Body.Close()
 
    body, _ := io.ReadAll(resp.Body)
    fmt.Printf("status: %d, body length: %d\n", resp.StatusCode, len(body))
}

In production

Always set a Timeout on http.Client - the zero value has no timeout and will hold a goroutine and a TCP connection open forever on a slow or unresponsive server. This has caused countless production incidents where a downstream service degradation cascaded into goroutine exhaustion in the caller. Always read and close resp.Body even when you do not need the content: failing to drain the body prevents the underlying TCP connection from returning to the pool, forcing a new connection for every request. Use http.NewRequestWithContext and pass a context derived from the incoming request so that if the upstream caller cancels (client disconnects, gateway times out), the outbound call also cancels. For services making many outbound calls, configure the http.Transport directly: increase MaxIdleConnsPerHost to match your concurrency, and set DisableKeepAlives: false (the default) to reuse connections. Closing the transport is rarely correct - share one client across the lifetime of the service.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free