Code of the Day
AdvancedProduction Go

net/http

Build HTTP servers with Go's standard library — HandleFunc, ServeMux, the Handler interface, middleware, and request/response types.

GoAdvanced13 min read
Recommended first
By the end of this lesson you will be able to:
  • Register a handler with http.HandleFunc and start a server with http.ListenAndServe
  • Explain the http.Handler interface and implement it on a custom type
  • Use http.NewServeMux to create an isolated router
  • Write a middleware function that wraps an http.Handler
  • Read path parameters, query strings, and request bodies

Go ships a production-grade HTTP server in its standard library. You don't need a framework to serve HTTP in Go — you need net/http. Many high-traffic Go services run on the standard library alone, or with only a thin router on top. Understanding net/http directly means you understand every framework built on it.

The minimal server

package main

import (
    "fmt"
    "net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello, world")
}

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

http.HandleFunc registers a handler function with the default ServeMux. ListenAndServe starts the server. Passing nil uses the default mux.

In production, always check the error from ListenAndServe — it only returns when the server stops:

if err := http.ListenAndServe(":8080", mux); err != nil {
    log.Fatal(err)
}

The http.Handler interface

The interface has one method:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Any type that implements ServeHTTP can be used as a handler. This is the extension point that makes and composable routers possible:

type AppHandler struct {
    db *sql.DB
}

func (h *AppHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // h.db is available here
    fmt.Fprintln(w, "handled")
}

http.NewServeMux — isolated routing

The default http.DefaultServeMux is a package-level global — a security risk if third-party packages register handlers on it. Use http.NewServeMux() instead:

mux := http.NewServeMux()
mux.HandleFunc("/health", healthHandler)
mux.HandleFunc("/users/", usersHandler)

http.ListenAndServe(":8080", mux)

Go 1.22 enhanced ServeMux to support method-based routing and path parameters:

mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)

For complex routing (named groups, regex, middleware trees), community routers like chi or gorilla/mux are popular — but they all implement http.Handler, so they compose with the standard library.

Reading requests

func userHandler(w http.ResponseWriter, r *http.Request) {
    // Path parameter (Go 1.22+)
    id := r.PathValue("id")

    // Query string: /users?filter=active
    filter := r.URL.Query().Get("filter")

    // Request body
    body, err := io.ReadAll(r.Body)
    defer r.Body.Close()
    if err != nil {
        http.Error(w, "bad request", http.StatusBadRequest)
        return
    }

    fmt.Fprintf(w, "user=%s filter=%s body=%s", id, filter, body)
}

Always close r.Body and always set an appropriate status code.

Writing 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)
}

Set headers before calling w.WriteHeader. Once WriteHeader is called, headers are sent and cannot be changed. Calling w.Write implicitly calls WriteHeader(200) if you haven't done so already.

Middleware

Middleware wraps a handler to add cross-cutting behaviour (logging, auth, tracing):

func logging(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))
    })
}

// Apply:
mux := http.NewServeMux()
mux.HandleFunc("/", hello)
http.ListenAndServe(":8080", logging(mux))

http.HandlerFunc is a type adapter that makes a function satisfy http.Handler. The pattern func(next http.Handler) http.Handler is the standard middleware signature — it lets you chain middleware with chain(auth(logging(mux))).

Check your understanding

Knowledge check

  1. 1.
    You want to pass your database connection into an HTTP handler. What is the idiomatic way?
  2. 2.
    Using http.DefaultServeMux is recommended in production because it is pre-configured for performance.
  3. 3.
    You call w.WriteHeader(201) and then w.Header().Set("Content-Type", "application/json"). What happens?

Do it yourself

Build a small HTTP server with two routes: GET /ping returns {"status":"ok"} and POST /echo returns whatever JSON body was sent. Add a logging middleware that prints method, path, and duration.

go run main.go &
curl localhost:8080/ping
curl -X POST localhost:8080/echo -d '{"hello":"world"}'

Where to go next

Your HTTP server is running. The next lesson covers JSON encoding — how to marshal structs to JSON, use struct tags, decode streams, and handle custom serialisation.

Finished reading? Mark it complete to track your progress.

On this page