Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Go HTTP middleware is a handler wrapper: it takes an http.Handler, adds behavior before or after that handler runs, and returns another handler. Because the wrapped handler still satisfies the same interface, you can stack middleware around an endpoint or an entire mux using Go’s standard library.

What Go HTTP middleware does

The net/http package defines a server-side handler through the http.Handler interface. Its ServeHTTP(http.ResponseWriter, *http.Request) method responds to an HTTP request. Middleware uses that same interface to put shared behavior around a handler, such as request logging, timing, authentication checks, request IDs, or response headers. Go does not provide those features automatically; middleware is a way to implement and reuse them.

A middleware function commonly has the shape func(http.Handler) http.Handler: it receives the next handler and returns a handler that can do work on either side of the call. Go’s net/http API documentation defines the handler contract, and Effective Go explains how values with a ServeHTTP method can serve HTTP requests.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How a request moves through middleware

Each wrapper receives the request first, can inspect or modify what it is permitted to, and then calls next.ServeHTTP(w, r) to pass the request inward. When the inner handler returns, the wrapper can do post-processing. In a chain such as outer(inner(endpoint)), the outer middleware’s pre-work runs first, while its post-work runs after the inner handler finishes.

This nesting order matters for behavior such as timing and logging. A timer started before calling the next handler can measure the time spent in everything inside that wrapper. A middleware that rejects a request before calling next prevents the inner handler from running.

Build and apply a simple middleware

This example logs the method and path before the endpoint handles a request, then logs when handling has completed:

package main

import (
    "log"
    "net/http"
)

func logRequests(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("started %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        log.Printf("finished %s %s", r.Method, r.URL.Path)
    })
}

func hello(w http.ResponseWriter, r *http.Request) {
    _, _ = w.Write([]byte("hellon"))
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /hello", hello)

    handler := logRequests(mux)
    log.Fatal(http.ListenAndServe(":8080", handler))
}
  1. Define the wrapper. logRequests accepts the downstream http.Handler and returns an http.Handler.

    Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  2. Adapt the inline function. http.HandlerFunc converts a function with the handler signature into a value implementing http.Handler.

  3. Call the next handler. next.ServeHTTP(w, r) hands the request to the mux; omitting that call is how middleware can deliberately stop the chain, for example after rejecting a request.

  4. Wrap the mux for global behavior. Passing logRequests(mux) to ListenAndServe applies this middleware to requests handled through that mux.

The route pattern GET /hello uses the method-and-path pattern syntax introduced in Go 1.22. That syntax concerns ServeMux routing, not middleware. The net/http documentation describes the current patterns and the compatibility setting GODEBUG=httpmuxgo121=1, which restores earlier mux behavior and is read when the process starts. The standard-library package documentation also explains that ListenAndServe uses the default mux when passed a nil handler; constructing an explicit mux makes the wrapper and route composition clear.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Compose more than one middleware layer

Wrap handlers from the inside out to make the resulting order explicit:

handler := requestID(recoverPanics(logRequests(mux)))

Here, requestID is outermost: it runs first on the way in and last after the inner chain returns. A helper that applies a slice of middleware can obscure this order unless its convention is documented. For example, this helper treats the first listed middleware as the outermost layer:

func chain(h http.Handler, middleware ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middleware) - 1; i >= 0; i-- {
        h = middleware[i](h)
    }
    return h
}

handler := chain(mux, requestID, recoverPanics, logRequests)

Write down or test the ordering convention when adding such a helper. Middleware that sets context or response headers may need to run before other layers, while timing or logging may need to surround them.

Middleware and handler details that can cause bugs

Finish response work before returning

A handler should write its reply and return. Do not keep using the http.ResponseWriter after ServeHTTP returns; its lifetime is limited to that call. Set response headers before the response is committed by writing the status or body. See the net/http handler documentation for the writer contract.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Read request bodies before writing responses

Generally, read the request body before writing a response if later code needs it. The API documentation cautions that, depending on the client, protocol version, and intermediaries, reading the body after writing to the response may not work. Avoid modifying the supplied request except where the API permits it; when downstream code needs a changed request, use the documented request-cloning or context patterns rather than assuming mutation is safe.

Be careful when wrapping ResponseWriter

A middleware wrapper may want to observe status codes or bytes written. A custom ResponseWriter wrapper can affect optional capabilities that downstream handlers expect, so a wrapper should preserve or correctly expose relevant interfaces rather than assuming every writer supports only the basic methods. There is no single minimal wrapper that is safe for every handler and every optional capability; check the API contract for the operations your stack uses.

Use panic recovery as a boundary, not as error handling

Recovery middleware can prevent a panic from escaping through a handler chain and can support a controlled server response or logging policy. It does not replace checking errors and returning appropriate responses in ordinary application code. The documented server behavior when a handler panics is described in the net/http documentation.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When standard-library middleware is enough

For a small service, composing http.Handler values directly keeps the dependency footprint low and makes wrapper order visible in code. A third-party router or framework may offer conveniences such as route-specific middleware or additional routing features, but those vary by project. Before adopting one, check its current documentation for how it orders global and per-route middleware and whether its response-writer handling fits the handlers you use.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Start with the standard interfaces when the needed behavior is simple and shared across routes. Add a router or framework when its concrete routing or registration features solve a problem your explicit mux and wrappers do not.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.