The Handler Interface in Go and Why Everything Is One
The one method behind net/http: http.Handler, the HandlerFunc adapter, routers, middleware and file servers as handlers, and tests with no server.

Most of what makes Go's net/http pleasant is one decision: the thing that handles a request is an interface with a single method. The router is one. The file server is one. The middleware you write, the authentication wrapper, the timeout, the endpoint that returns JSON: each one is something with a ServeHTTP method. Once you see that, the design becomes predictable. Composition is wrapping. Testing is calling a method. A framework adds features by implementing the same interface, and anything you wrote for the standard library works with it.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
This article lays out the interface, the adapter that makes functions fit it, the places in the standard library where a handler hides, and what it means for dependencies and tests. It is the foundation for middleware and for the router patterns, follows your first HTTP server, and belongs to the web cluster that ends in a production HTTP server. Everything ran on Go 1.25.6.
The interface
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}That is the whole contract, from the net/http source. The server calls ServeHTTP with a writer for the response and the parsed request. When the method returns, the response is complete. There is no return value: output goes through the writer, and errors are handled by writing an error response. The ResponseWriter is itself a small interface (Header(), Write([]byte), WriteHeader(int)), which keeps the contract narrow enough to wrap, as in wrapping the ResponseWriter.
Writing a handler as a type is the long form:
type hello struct{}
func (hello) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "hello")
}
http.Handle("/hello", hello{})That is more ceremony than most endpoints deserve, which is why the standard library supplies the adapter.
HandlerFunc: a function that is a Handler
http.HandlerFunc is a function type with a method:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }The method calls the function itself. Converting an ordinary function to HandlerFunc produces a value that satisfies Handler, and http.HandleFunc(pattern, fn) does that conversion for you. This is the same trick as sort.Interface adapters and io.Writer wrappers elsewhere: define the behavior as a function, attach the interface to the function type. It is the reason most Go handlers are plain functions and still compose with everything that wants a Handler.
The two forms are interchangeable at the registration site:
mux.HandleFunc("GET /a", func(w http.ResponseWriter, r *http.Request) { ... }) // takes a function
mux.Handle("GET /b", http.HandlerFunc(aFunction)) // takes a HandlerWhere handlers hide in the standard library
The practical payoff is how many things you meet are handlers, and therefore interchangeable and nestable. This program calls five different ones through the same interface, using a recorder, so no network is needed:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"testing/fstest"
"time"
)
// A handler as a struct: its dependencies are fields, not globals.
type greeter struct{ greeting string }
func (g greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, %s", g.greeting, r.URL.Query().Get("name"))
}
func call(name string, h http.Handler, method, target string) {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(method, target, nil)) // a method call, no listener
fmt.Printf("%-22s %d %q\n", name, rec.Code, rec.Body.String())
}
func main() {
fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "from a function") })
mux := http.NewServeMux() // a router is a Handler
mux.Handle("/greet", greeter{greeting: "hello"})
mux.Handle("GET /fn", fn)
files := http.FileServerFS(fstest.MapFS{"hello.txt": {Data: []byte("file contents")}})
slow := http.TimeoutHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(200 * time.Millisecond)
fmt.Fprint(w, "too late")
}), 20*time.Millisecond, "timed out")
call("HandlerFunc", fn, "GET", "/")
call("struct handler", greeter{greeting: "hi"}, "GET", "/?name=ada")
call("ServeMux dispatching", mux, "GET", "/greet?name=grace")
call("ServeMux, wrong method", mux, "POST", "/fn")
call("FileServerFS", files, "GET", "/hello.txt")
call("StripPrefix + files", http.StripPrefix("/static", files), "GET", "/static/hello.txt")
call("TimeoutHandler", slow, "GET", "/")
call("NotFoundHandler", http.NotFoundHandler(), "GET", "/")
}HandlerFunc 200 "from a function"
struct handler 200 "hi, ada"
ServeMux dispatching 200 "hello, grace"
ServeMux, wrong method 405 "Method Not Allowed\n"
FileServerFS 200 "file contents"
StripPrefix + files 200 "file contents"
TimeoutHandler 503 "timed out"
NotFoundHandler 404 "404 page not found\n"Read the list. A function, a struct, a router, a file server, a prefix stripper, a timeout wrapper and the stock 404 handler all went through the identical call h.ServeHTTP(rec, req). ServeMux matched the path and method and delegated to the registered handler, returning 405 for the wrong method. StripPrefix took another handler and returned a new one that rewrites the path before calling it. TimeoutHandler ran the inner handler with a deadline and replaced a slow response with its own. Each wrapper takes a Handler and returns a Handler, which is the shape of all middleware: func(http.Handler) http.Handler.
Dependencies without globals
The struct form is how handlers get what they need: a database handle, a logger, a template set. The struct holds the dependencies, and the method uses them:
type Server struct {
db *sql.DB
logger *slog.Logger
}
func (s *Server) handleOrder(w http.ResponseWriter, r *http.Request) {
// s.db and s.logger are in scope; no package-level variables
}
mux.HandleFunc("GET /orders/{id}", s.handleOrder) // a method value is a func(w, r)A method value, s.handleOrder, is a function that has captured s, so it can be passed anywhere a func(ResponseWriter, *Request) is wanted. This is the idiom for real servers: one Server struct, many methods, a routes() function that registers them. Tests construct a Server with a fake database and call the method, with no globals to reset between tests.
Testing needs no server
Because a handler is a method, httptest.NewRecorder stands in for the ResponseWriter and httptest.NewRequest builds a request without a network. The program above did exactly that. A unit test of an endpoint is a few lines:
rec := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/orders/42", nil)
srv.routes().ServeHTTP(rec, req) // exercises routing and the handler together
if rec.Code != http.StatusOK { t.Fatalf("status %d", rec.Code) }Calling the whole router, not only the inner handler, tests path matching and middleware too, which is where many bugs live. No port is opened, no goroutine is started, and the test is fast enough to run thousands of times. Use httptest.NewServer only when the code under test needs a real connection: an HTTP client, a redirect, timeouts, TLS.
An interface the server can see through
One last consequence of the interface being small: other parts of the standard library can wrap it without knowing your type. http.ResponseController (Go 1.20) reaches through wrappers that implement Unwrap() http.ResponseWriter to get at flushing, deadlines and hijacking, so a wrapper you write does not hide those capabilities from the code below it, provided you add the one-line Unwrap method (ResponseController). The same narrowness is why third-party routers and frameworks interoperate: Chi, Gin's http.Handler adapter, gorilla/mux and the standard mux all accept and return handlers, so a middleware written for one works in another with no glue.
Writing a wrapper yourself
The standard wrappers are written the same way you would write your own. A function that takes a handler and returns a handler, with the inner call in the middle:
func withHeader(key, value string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set(key, value) // before the inner handler writes its body
next.ServeHTTP(w, r)
})
}
mux := http.NewServeMux()
mux.Handle("GET /fn", withHeader("X-Frame-Options", "DENY", fn))Everything outside withHeader sees a Handler, and so does everything inside, which is why wrappers stack in any order. The middleware article builds chains of these, adds a response wrapper that can see the status code, and covers the ordering rules.
Rules worth knowing
A handler must not use the request or writer after it returns. The server reuses them. Goroutines started by a handler that touch r or w later are a data race and a source of corrupted responses.
Handlers run concurrently. One ServeHTTP call runs per request, so a handler's struct fields are shared by all of them. Read-only fields are safe; mutable state needs a lock (data races).
The writer is not safe for concurrent use. Write from one goroutine.
Write the status once, before the body. Calling WriteHeader after writing, or twice, logs "superfluous response.WriteHeader" and has no effect (status codes).
Panics are caught per request by the server, which logs and closes the connection. Add your own recovery if you want a clean 500 (recovery middleware).
The pattern that follows from all this, and that the rest of the web cluster builds on, is: small handlers, dependencies in structs, behavior added by wrapping, and tests that call the router directly. The Go blog's post on routing enhancements explains how the standard router builds on the same interface.


