net/http Standard Library

Updated

September 8, 2026

net/http Standard Library

Overview

net/http is a complete HTTP client and server. Since Go 1.22, the standard multiplexer supports method-aware patterns (GET /items/{id}), which covers most API routing without a framework.

Resilience deep dive: HTTP client/server resilience.

Server (Go 1.22+ patterns)

mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    _, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    fmt.Fprintln(w, id)
})
mux.HandleFunc("POST /items", createItem)

srv := &http.Server{
    Addr:              ":8080",
    Handler:           mux,
    ReadHeaderTimeout: 5 * time.Second,
    ReadTimeout:       15 * time.Second,
    WriteTimeout:      30 * time.Second,
    IdleTimeout:       60 * time.Second,
}
log.Fatal(srv.ListenAndServe())

Always set timeouts. Defaults can hang forever under slow clients.

Middleware pattern

func withRequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := r.Header.Get("X-Request-ID")
        if id == "" {
            id = strconv.FormatInt(time.Now().UnixNano(), 36)
        }
        w.Header().Set("X-Request-ID", id)
        ctx := context.WithValue(r.Context(), ctxKeyID, id)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Client

client := &http.Client{
    Timeout: 10 * time.Second,
    Transport: &http.Transport{
        Proxy:                 http.ProxyFromEnvironment,
        DialContext:           (&net.Dialer{Timeout: 3 * time.Second}).DialContext,
        MaxIdleConns:          100,
        IdleConnTimeout:       90 * time.Second,
        TLSHandshakeTimeout:   5 * time.Second,
        ForceAttemptHTTP2:     true,
    },
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
    return err
}
defer resp.Body.Close()
body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))

Never use the bare package-level http.Get in production code without timeouts — it uses http.DefaultClient with no overall timeout.

JSON helpers

func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(status)
    _ = json.NewEncoder(w).Encode(v)
}

func readJSON(r *http.Request, dst any) error {
    defer r.Body.Close()
    dec := json.NewDecoder(io.LimitReader(r.Body, 1<<20))
    dec.DisallowUnknownFields()
    return dec.Decode(dst)
}

net/url

u, err := url.Parse("https://example.com/search?q=go+lang")
q := u.Query()
q.Set("page", "2")
u.RawQuery = q.Encode()

httptest (for unit tests)

req := httptest.NewRequest(http.MethodGet, "/health", nil)
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
if rr.Code != 200 { t.Fatal(rr.Code) }

Runnable example

go mod init example
go run .
package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "net/http/httptest"
    "strings"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
    })
    mux.HandleFunc("GET /echo/{msg}", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, r.PathValue("msg"))
    })
    mux.HandleFunc("POST /sum", func(w http.ResponseWriter, r *http.Request) {
        var in struct{ A, B int }
        if err := json.NewDecoder(io.LimitReader(r.Body, 4096)).Decode(&in); err != nil {
            http.Error(w, "bad json", http.StatusBadRequest)
            return
        }
        _ = json.NewEncoder(w).Encode(map[string]int{"sum": in.A + in.B})
    })

    // health
    req := httptest.NewRequest(http.MethodGet, "/health", nil)
    rr := httptest.NewRecorder()
    mux.ServeHTTP(rr, req)
    fmt.Println("health", rr.Code, strings.TrimSpace(rr.Body.String()))

    // path value
    req = httptest.NewRequest(http.MethodGet, "/echo/hi", nil)
    rr = httptest.NewRecorder()
    mux.ServeHTTP(rr, req)
    fmt.Println("echo", rr.Body.String())

    // POST JSON
    body := strings.NewReader(`{"A":2,"B":40}`)
    req = httptest.NewRequest(http.MethodPost, "/sum", body)
    req.Header.Set("Content-Type", "application/json")
    rr = httptest.NewRecorder()
    mux.ServeHTTP(rr, req)
    fmt.Println("sum", strings.TrimSpace(rr.Body.String()))
}

Expected output:

health 200 {"status":"ok"}
echo hi
sum {"sum":42}

What to notice: - Method + path patterns keep routing in the stdlib. - httptest exercises handlers without binding a port. - Body decoding should always be size-limited.

Try next: Add middleware that rejects requests missing Authorization with 401, and test it with httptest.