net/http Standard Library
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.