iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

net/http

net/http is Go’s built-in HTTP server and client — production-ready out of the box. With ServeMux, middleware via handler wrappers, and context.Context threaded through requests, you can build APIs without a framework.

Server, mux, middleware, client

EXAMPLE
package main

import (
    "context"
    "encoding/json"
    "errors"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

// 1) Minimal server
func hello(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain")
    w.Write([]byte("hello world"))
}

func main_simple() {
    http.HandleFunc("/hello", hello)
    http.ListenAndServe(":8080", nil)
}

// 2) ServeMux 1.22+ — pattern routing with methods + path params
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /",          home)
    mux.HandleFunc("GET /users",     listUsers)
    mux.HandleFunc("GET /users/{id}", getUser)
    mux.HandleFunc("POST /users",    createUser)
    mux.HandleFunc("DELETE /users/{id}", deleteUser)

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

    // Graceful shutdown
    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
    <-sig
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    srv.Shutdown(ctx)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    w.Write([]byte("user " + id))
}

// 3) JSON responses
func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

func listUsers(w http.ResponseWriter, r *http.Request) {
    writeJSON(w, http.StatusOK, []map[string]any{
        {"id": 1, "name": "Mara"},
        {"id": 2, "name": "Sam"},
    })
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var body struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }
    if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
        writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
        return
    }
    writeJSON(w, http.StatusCreated, map[string]any{"id": 42, "name": body.Name})
}

// 4) Middleware via handler wrappers
func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        sw := &statusWriter{ResponseWriter: w, status: 200}
        next.ServeHTTP(sw, r)
        slog.Info("http", "method", r.Method, "path", r.URL.Path, "status", sw.status, "ms", time.Since(start).Milliseconds())
    })
}

func recovery(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                slog.Error("panic", "err", rec)
                http.Error(w, "internal error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

type statusWriter struct {
    http.ResponseWriter
    status int
}

func (s *statusWriter) WriteHeader(code int) {
    s.status = code
    s.ResponseWriter.WriteHeader(code)
}

// 5) Auth middleware
func requireAuth(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        // verify token, attach user to context
        ctx := context.WithValue(r.Context(), "userID", "42")
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// 6) HTTP client
func fetchUser(ctx context.Context, id string) (*User, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/users/"+id, nil)
    if err != nil { return nil, err }
    req.Header.Set("Accept", "application/json")

    client := &http.Client{ Timeout: 10 * time.Second }
    resp, err := client.Do(req)
    if err != nil { return nil, err }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, errors.New("non-200: " + resp.Status)
    }

    var u User
    if err := json.NewDecoder(resp.Body).Decode(&u); err != nil {
        return nil, err
    }
    return &u, nil
}

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// 7) Custom Transport — connection pool tuning
client := &http.Client{
    Timeout: 30 * time.Second,
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:     90 * time.Second,
        DisableCompression:  false,
    },
}

// REUSE a single client across the program. Don't create per request.

// 8) CORS — write your own (or use 'rs/cors')
func cors(allowed []string) func(http.Handler) http.Handler {
    set := make(map[string]bool)
    for _, o := range allowed { set[o] = true }
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            origin := r.Header.Get("Origin")
            if set[origin] {
                w.Header().Set("Access-Control-Allow-Origin", origin)
                w.Header().Set("Vary", "Origin")
                w.Header().Set("Access-Control-Allow-Credentials", "true")
            }
            if r.Method == http.MethodOptions {
                w.Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE")
                w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
                w.WriteHeader(http.StatusNoContent)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

// 9) Static files
fs := http.FileServer(http.Dir("./public"))
mux.Handle("GET /static/", http.StripPrefix("/static/", fs))

// 10) HTTPS with autocert
import "golang.org/x/crypto/acme/autocert"
m := &autocert.Manager{
    Cache:      autocert.DirCache("/var/www/.cache"),
    Prompt:     autocert.AcceptTOS,
    HostPolicy: autocert.HostWhitelist("example.com"),
}
srv := &http.Server{
    Addr:      ":443",
    Handler:   mux,
    TLSConfig: m.TLSConfig(),
}
srv.ListenAndServeTLS("", "")

// 11) Frameworks — when stdlib isn't enough
// • chi — lightweight router, idiomatic Go
// • gin — fast, opinionated, middleware ecosystem
// • echo — high performance, batteries-included
// • Fiber — Express-like API, fasthttp under the hood
//
// stdlib + a router (chi/mux) is plenty for most APIs. Skip frameworks until proven need.

// 12) Common bugs
// • Forgetting defer resp.Body.Close() → connection leak
// • Reusing http.Request for retries — body is consumed; clone or recreate
// • No timeouts on http.Client → hangs forever on slow upstream
// • Mux without method routing — handle 405 manually pre Go 1.22
// • Reading r.Body after calling r.ParseForm — body already consumed
// • Writing headers after WriteHeader → silently dropped; set headers FIRST
// • Forgot WithContext when calling external APIs → no cancellation
// • DefaultClient + DefaultTransport — fine for scripts; tune for prod
// • Sharing context across requests — each request has its own; use request context
// • Returning errors as 200 + JSON body — set proper status codes

Why it matters

Go’s net/http is production-ready: ServeMux 1.22+ handles method routing and path params, middleware via handler wrappers covers logging/recovery/CORS, and context.Context threads cancellation everywhere. Always set server timeouts, set http.Client.Timeout, reuse a single client, and defer resp.Body.Close().

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
http.HandleFunc("/hi", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello")
})
log.Fatal(http.ListenAndServe(":8080", nil))
Try it Yourself »

Discussion

Loading…