V
Vel·ToolKit
Simple · Fast · Ready to use
EN
Chapter 19 of 20

net/http

HTTP server, client, middleware, graceful shutdown

Minimal HTTP Server

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
    })
    http.ListenAndServe(":8080", nil)
}

Routing and ServeMux (Go 1.22)

Since Go 1.22, net/http's ServeMux supports method prefixes and path parameters; previously you needed libraries like chi / gorilla mux. The built-in mux is enough for everyday CRUD; reach for chi / gin only for complex routing.

package main

import (
    "fmt"
    "log"
    "net/http"
)

func createUser(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "create user")
}

func deleteUser(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "delete user:", r.PathValue("id"))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")           // path parameter
        q := r.URL.Query().Get("verbose") // query string
        fmt.Fprintln(w, "user:", id, q)
    })
    mux.HandleFunc("POST /users", createUser)
    mux.HandleFunc("DELETE /users/{id}", deleteUser)
    log.Fatal(http.ListenAndServe(":8080", mux))
}

Server Configuration (Don't Call ListenAndServe Directly)

A production service must always construct http.Server explicitly, for two reasons: 1) only then can you set layered timeouts; 2) only then can you do graceful shutdown. http.ListenAndServe has no timeouts at all, so a malicious slow connection can tie up your process for a long time.

package main

import (
    "errors"
    "fmt"
    "log"
    "net/http"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "ok")
    })

    srv := &http.Server{
        Addr:              ":8080",
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,   // prevent Slowloris
        ReadTimeout:       30 * time.Second,  // upper bound to read the whole request body
        WriteTimeout:      30 * time.Second,  // upper bound to write the response
        IdleTimeout:       120 * time.Second, // keep-alive idle limit
        MaxHeaderBytes:    1 << 20,
    }
    if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}

Graceful Shutdown

On SIGINT/SIGTERM, Shutdown stops accepting new connections, waits for in-flight requests to finish, then exits. Pair it with a context deadline so a "never-finishing" request can't block the process forever.

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "ok")
    })
    srv := &http.Server{Addr: ":8080", Handler: mux}

    go func() {
        if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            log.Fatal(err)
        }
    }()

    stop := make(chan os.Signal, 1)
    signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
    <-stop
    log.Println("shutting down...")

    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Printf("graceful shutdown: %v", err)
    }
}

JSON API

package main

import (
    "context"
    "encoding/json"
    "log"
    "net/http"
    "sync"
)

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

type Store struct {
    mu sync.Mutex
    n  int
}

func (s *Store) Create(_ context.Context, u *User) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.n++
    u.ID = s.n
    return nil
}

func main() {
    store := &Store{}
    mux := http.NewServeMux()
    mux.HandleFunc("POST /api/users", func(w http.ResponseWriter, r *http.Request) {
        var u User
        dec := json.NewDecoder(r.Body)
        dec.DisallowUnknownFields() // strict mode
        if err := dec.Decode(&u); err != nil {
            http.Error(w, err.Error(), http.StatusBadRequest)
            return
        }
        if err := store.Create(r.Context(), &u); err != nil {
            http.Error(w, "save failed", http.StatusInternalServerError)
            return
        }
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusCreated)
        _ = json.NewEncoder(w).Encode(u)
    })
    log.Fatal(http.ListenAndServe(":8080", mux))
}

context in a Request

Every *http.Request carries a context: it's cancelled when the client disconnects or the Server calls Shutdown. Pass it on to DB queries and RPC calls so downstream can exit early too.

package main

import (
    "context"
    "database/sql"
    "errors"
    "log"
    "net/http"
    "time"

    _ "modernc.org/sqlite"
)

var db *sql.DB

func handler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()
    rows, err := db.QueryContext(ctx, "SELECT 1")
    if err != nil {
        if errors.Is(err, context.Canceled) {
            return // client disconnected; no need to report an error
        }
        http.Error(w, err.Error(), 500)
        return
    }
    defer rows.Close()
    w.Write([]byte("ok"))
}

func main() {
    var err error
    db, err = sql.Open("sqlite", ":memory:")
    if err != nil {
        log.Fatal(err)
    }
    http.HandleFunc("/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Middleware Chain

Middleware is just a function that "takes a Handler and returns a Handler". To compose several, wrap them by hand layer by layer — no framework needed.

package main

import (
    "fmt"
    "log"
    "net/http"
    "runtime/debug"
    "time"
)

func logger(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

func recoverer(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("panic: %v\n%s", rec, debug.Stack())
                http.Error(w, "internal", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "hello")
    })

    // compose: recover first, then log
    handler := recoverer(logger(mux))
    log.Fatal(http.ListenAndServe(":8080", handler))
}

HTTP Client: Reuse the Transport

http.Client is thread-safe and should be reused globally — don't new one per request; reusing the Transport's underlying connection pool avoids a handshake every time. Setting timeouts and connection limits on the client is the watershed between "production code vs toy code".

package main

import (
    "context"
    "fmt"
    "io"
    "log"
    "net"
    "net/http"
    "time"
)

var httpClient = &http.Client{
    Timeout: 10 * time.Second, // total timeout for the whole request (connect, read, redirects)
    Transport: &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 20,
        IdleConnTimeout:     90 * time.Second,
        DialContext: (&net.Dialer{
            Timeout:   3 * time.Second, // TCP connection timeout
            KeepAlive: 30 * time.Second,
        }).DialContext,
        TLSHandshakeTimeout: 5 * time.Second,
    },
}

func fetch(ctx context.Context, url, token string) error {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    req.Header.Set("Authorization", "Bearer "+token)
    resp, err := httpClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode/100 != 2 {
        body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<10))
        return fmt.Errorf("upstream %d: %s", resp.StatusCode, body)
    }
    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
    return nil
}

func main() {
    if err := fetch(context.Background(), "https://example.com", "secret"); err != nil {
        log.Println("err:", err)
    }
}

multipart File Upload

package main

import (
    "bytes"
    "fmt"
    "io"
    "log"
    "mime/multipart"
    "net/http"
    "os"
    "path/filepath"
)

func upload(w http.ResponseWriter, r *http.Request) {
    if err := r.ParseMultipartForm(32 << 20); err != nil { // 32 MB in-memory threshold
        http.Error(w, err.Error(), 400)
        return
    }
    file, header, err := r.FormFile("file")
    if err != nil {
        http.Error(w, err.Error(), 400)
        return
    }
    defer file.Close()

    dst, err := os.Create(filepath.Join(os.TempDir(), header.Filename))
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    defer dst.Close()
    io.Copy(dst, file)
    fmt.Fprintln(w, "ok", header.Size)
}

func postFile(url, path string) error {
    src, err := os.Open(path)
    if err != nil {
        return err
    }
    defer src.Close()

    body := &bytes.Buffer{}
    mw := multipart.NewWriter(body)
    fw, _ := mw.CreateFormFile("file", filepath.Base(path))
    io.Copy(fw, src)
    mw.Close()

    req, _ := http.NewRequest("POST", url, body)
    req.Header.Set("Content-Type", mw.FormDataContentType())
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    return nil
}

func main() {
    http.HandleFunc("/upload", upload)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Static File Serving

package main

import (
    "log"
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./public"))))

    // a single file
    mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
        http.ServeFile(w, r, "./public/favicon.ico")
    })

    log.Fatal(http.ListenAndServe(":8080", mux))
}