HTTP Client
Go's net/http client is production-grade — no third-party library required for most API integrations.
Introduction
Go's net/http client is production-grade — no third-party library required for most API integrations. Create a reusable http.Client with timeout, build requests with http.NewRequestWithContext, and always close response bodies.
Custom Transport configures TLS, connection pooling, and proxies. Interviewers ask about client timeout vs context timeout, retry strategies, and connection reuse — patterns used when calling payment gateways, internal microservices, and cloud APIs.
This lesson builds a production HTTP client with context cancellation, structured error handling, and JSON decoding.
The story
A service mesh sidecar calls upstream health endpoints through an http.Client with a 2-second timeout, custom TLS config for mTLS, and retry logic for transient 503 responses from overloaded API gateways. Uber's internal Go services share tuned Transport instances with connection pooling — creating a new Client per request exhausts file descriptors.
The default client has no timeout — always configure Timeout or context deadlines to prevent goroutine leaks during upstream outages.
Understanding the topic
Key concepts
- http.Client{Timeout: d} sets total request timeout.
- NewRequestWithContext attaches cancellation to outbound call.
- resp.Body must be closed — defer resp.Body.Close().
- Default Client is shared — prefer custom Client for timeouts.
- Transport controls connection pool, TLS, keep-alive.
- Check resp.StatusCode — 4xx/5xx are not Go errors from Do().
sequenceDiagramClient->>Server: HTTP RequestServer->>Handler: ServeHTTPHandler->>Service: Business LogicService->>DB: QueryDB-->>Handler: DataHandler-->>Client: JSON Response
Step-by-step explanation
- Build request with method, URL, body, headers.
- client.Do(req) executes request, returns *Response.
- Read body with io.ReadAll or json.Decoder.
- Context cancel aborts in-flight request.
- Transport.MaxIdleConnsPerHost tunes connection reuse.
- http.DefaultClient has no timeout — dangerous in production.
Practical code example
Production HTTP client calling JSON API with context and timeout:
package mainimport ("context""encoding/json""fmt""io""net/http""time")type APIClient struct {http *http.Clientbase string}func NewAPIClient(base string) *APIClient {return &APIClient{base: base,http: &http.Client{Timeout: 10 * time.Second},}}func (c *APIClient) GetUser(ctx context.Context, id string) (map[string]any, error) {req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.base+"/users/"+id, nil)if err != nil {return nil, fmt.Errorf("build request: %w", err)}req.Header.Set("Accept", "application/json")resp, err := c.http.Do(req)if err != nil {return nil, fmt.Errorf("do request: %w", err)}defer resp.Body.Close()if resp.StatusCode != http.StatusOK {body, _ := io.ReadAll(io.LimitReader(resp.Body, 1024))return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, body)}var result map[string]anyif err := json.NewDecoder(resp.Body).Decode(&result); err != nil {return nil, fmt.Errorf("decode json: %w", err)}return result, nil}func main() {ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()client := NewAPIClient("https://jsonplaceholder.typicode.com")user, err := client.GetUser(ctx, "1")if err != nil {fmt.Println("error:", err)return}fmt.Printf("user: %v\n", user["name"])}
Line-by-line code explanation
client := &http.Client{Timeout: 2 * time.Second}sets a global request timeout.req, err := http.NewRequestWithContext(ctx, "GET", url, nil)builds a cancellable request.resp, err := client.Do(req)executes the request and returns the response.defer resp.Body.Close()releases the connection back to the pool — mandatory.io.ReadAll(resp.Body)reads the response body — don't forget to check status code first.resp.StatusCodeholds the HTTP status — 2xx success, 4xx client error, 5xx server error.transport := &http.Transport{MaxIdleConns: 100}tunes connection pooling for high throughput.client.Transport = transportattaches custom transport settings to reuse TCP connections.
Key takeaway: Never use http.Get without timeout in production. Always close Body. Check status code explicitly.
Real-world use
Where you'll use this in production
- Calling Stripe, Twilio, and AWS APIs from Go services.
- Service-to-service communication in microservice mesh.
- Health check probes against dependent services.
- Webhook delivery with retry and exponential backoff.
Best practices
- Reuse http.Client — don't create per request.
- Set Client.Timeout and use context for per-request deadlines.
- Close response body always; use LimitReader on error bodies.
- Configure Transport for production connection pooling.
- Wrap errors with context; log status and correlation ID.
- Use structured client type, not scattered http.Get calls.
Common mistakes
- Using http.DefaultClient — no timeout, hangs forever.
- Not closing resp.Body — connection leak.
- Ignoring non-2xx status codes.
- Creating new Client per request — loses connection pool.
- Not propagating context from incoming HTTP request.
Advanced interview questions
Q1BeginnerClient.Timeout vs context?
Q2BeginnerMust close response Body?
Q3IntermediateRetry failed HTTP calls?
Q4IntermediateConfigure connection pool?
Q5AdvancedDesign resilient client for flaky payment API.
Summary
Use custom http.Client with Timeout — never DefaultClient in production. NewRequestWithContext propagates cancellation. Always close Body; check StatusCode explicitly. Reuse Client for connection pooling across requests. Next lesson: HTTP server with net/http.