Go (Golang) Tutorial 0/45 lessons ~6 min read Lesson 30

    HTTP Client

    Go's net/http client is production-grade — no third-party library required for most API integrations.

    Course progress0%
    Focus
    10 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    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().
    text
    sequenceDiagram
    Client->>Server: HTTP Request
    Server->>Handler: ServeHTTP
    Handler->>Service: Business Logic
    Service->>DB: Query
    DB-->>Handler: Data
    Handler-->>Client: JSON Response

    Step-by-step explanation

    1. Build request with method, URL, body, headers.
    2. client.Do(req) executes request, returns *Response.
    3. Read body with io.ReadAll or json.Decoder.
    4. Context cancel aborts in-flight request.
    5. Transport.MaxIdleConnsPerHost tunes connection reuse.
    6. http.DefaultClient has no timeout — dangerous in production.

    Practical code example

    Production HTTP client calling JSON API with context and timeout:

    go
    package main
    import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "time"
    )
    type APIClient struct {
    http *http.Client
    base 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]any
    if 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.StatusCode holds the HTTP status — 2xx success, 4xx client error, 5xx server error.
    • transport := &http.Transport{MaxIdleConns: 100} tunes connection pooling for high throughput.
    • client.Transport = transport attaches 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?
    Client.Timeout total limit; context finer per-request cancel and deadline propagation.
    Q2BeginnerMust close response Body?
    Yes — otherwise connection not returned to pool.
    Q3IntermediateRetry failed HTTP calls?
    Retry idempotent methods with backoff; respect Retry-After header; use context.
    Q4IntermediateConfigure connection pool?
    Transport MaxIdleConns, MaxIdleConnsPerHost, IdleConnTimeout.
    Q5AdvancedDesign resilient client for flaky payment API.
    Timeout, 3 retries with jitter, circuit breaker, idempotency key header, structured logging.

    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.

    Ready to mark this lesson complete?Track your journey across the entire course.