How http.Handler and http.HandlerFunc fit together
You write a Go web server. You use http.HandleFunc to wire up a route, pass a function, and the server starts. It works. Then you try to use a third-party router or write middleware. The API asks for an http.Handler. You pass your function. The compiler rejects the code. You wonder why Go requires a special interface when a function seems perfectly capable of handling a request.
The confusion comes from Go's separation of behavior and implementation. http.Handler defines the contract for anything that can process an HTTP request. http.HandlerFunc is a bridge that lets you treat a plain function as if it were that contract. Understanding this distinction unlocks middleware, composition, and clean server architecture.
The interface and the adapter
http.Handler is an interface with a single method:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Any type that implements ServeHTTP satisfies this interface. Go uses structural typing, so you do not declare implements. If your type has the method, it is a handler.
Functions do not have methods by default. A function signature like func(http.ResponseWriter, *http.Request) is not a struct and cannot implement an interface. http.HandlerFunc solves this. It is a named function type that carries a ServeHTTP method. The standard library defines it roughly as:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
When you cast a function to http.HandlerFunc, you get a value that implements http.Handler. The adapter calls your function when ServeHTTP is invoked. This pattern lets you use functions anywhere a handler is expected without writing boilerplate structs for every route.
Interfaces define behavior. Structs hold state. The adapter connects the two.
Minimal example
Here's the simplest way to bridge a function to the handler interface.
package main
import (
"fmt"
"net/http"
)
// handleRequest writes the path to the response.
func handleRequest(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Path: %s", r.URL.Path)
}
func main() {
// Convert the function to a handler via the adapter type.
handler := http.HandlerFunc(handleRequest)
// ListenAndServe expects an http.Handler interface.
http.ListenAndServe(":8080", handler)
}
The http.HandlerFunc(handleRequest) expression returns a value of type http.HandlerFunc. That value has a ServeHTTP method, so it satisfies http.Handler. ListenAndServe accepts the interface and calls ServeHTTP for every incoming request.
If you skip the adapter and pass the function directly, the compiler rejects the program with cannot use handleRequest (func(http.ResponseWriter, *http.Request)) as net/http.Handler value in argument: func(http.ResponseWriter, *http.Request) does not implement net/http.Handler (missing ServeHTTP method). The error message tells you exactly what is missing: the method.
Trust gofmt. The indentation and layout in these examples follow the standard tool. Most editors run it on save.
What happens under the hood
At compile time, the type checker verifies that http.HandlerFunc implements http.Handler. It sees the ServeHTTP method on the named function type and marks the interface as satisfied. No reflection is involved. The check is static and fast.
At runtime, ListenAndServe holds a value of type http.Handler. When a request arrives, it calls handler.ServeHTTP(w, r). Because the underlying value is an http.HandlerFunc, the call dispatches to the method defined on that type. The method body simply invokes the wrapped function. The overhead is a single function call indirection, which is negligible compared to network I/O.
This design enables composition. Since http.Handler is an interface, you can wrap handlers, chain them, and swap implementations without changing the calling code. Middleware works by returning a new http.Handler that delegates to the original handler after performing side effects.
Interfaces are implicit. If it has ServeHTTP, it is a handler.
Realistic example with state and context
Functions work for simple routes. Real applications need configuration, database connections, or shared state. Structs let you carry data across requests. Here's a handler that tracks request counts and respects context cancellation.
package main
import (
"context"
"fmt"
"net/http"
"sync/atomic"
)
// App holds server state like a request counter.
type App struct {
// count tracks total requests atomically.
count atomic.Int64
}
// ServeHTTP implements the http.Handler interface.
// The receiver name 'a' follows Go convention for short identifiers.
func (a *App) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Extract context for cancellation and deadlines.
ctx := r.Context()
// Check if the request was cancelled before doing work.
if err := ctx.Err(); err != nil {
return
}
// Increment counter safely for concurrent access.
n := a.count.Add(1)
fmt.Fprintf(w, "Request #%d to %s", n, r.URL.Path)
}
func main() {
// Create the app instance with state.
app := &App{}
// The struct pointer implements http.Handler directly.
http.ListenAndServe(":8080", app)
}
The App struct implements http.Handler by defining ServeHTTP. The receiver is a pointer so the handler can mutate the counter. The receiver name a matches the type App, following the convention of using one or two letters for receivers.
The handler extracts the context from the request. Context is plumbing. Run it through every long-lived call site. Checking ctx.Err() early prevents wasted work if the client disconnects. The community accepts the if err != nil boilerplate because it makes the unhappy path visible.
Pitfalls and patterns
A common mistake is confusing http.HandleFunc with http.Handler. HandleFunc is a method on ServeMux, the default router. It takes a function and internally converts it to http.HandlerFunc before registering it. Handle is the sibling method that takes an http.Handler directly. Use Handle when you have a struct handler or middleware. Use HandleFunc for quick function registration.
Middleware often trips up beginners. Middleware is a function that takes an http.Handler and returns an http.Handler. The return value wraps the input handler. Here's a logging middleware:
// loggingMiddleware wraps a handler to log requests.
// It returns an http.Handler so it can be chained.
func loggingMiddleware(next http.Handler) http.Handler {
// Return a handler that logs, then calls the next handler.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Request:", r.URL.Path)
next.ServeHTTP(w, r)
})
}
The middleware returns http.HandlerFunc because the inner logic is a function. The wrapper satisfies http.Handler and can be passed to ListenAndServe or another middleware layer. Middleware is composition. Wrap handlers, don't nest functions.
Another pitfall is forgetting that ServeHTTP runs concurrently for each request. If your handler shares state, protect it with mutexes or atomic operations. The atomic.Int64 in the example handles the counter safely. Goroutine leaks happen when a goroutine waits on a channel that never gets closed. Always have a cancellation path, usually via context.
The worst goroutine bug is the one that never logs.
Decision matrix
Use http.HandleFunc when you are wiring up simple routes on a ServeMux and do not need to carry state or apply middleware programmatically.
Use http.HandlerFunc when you need to pass a function where an http.Handler interface is expected, such as in middleware return values or custom router arguments.
Use a struct with ServeHTTP when your handler needs configuration, database connections, or shared state across requests.
Use http.Handler as a parameter type when writing middleware or libraries that accept any compatible handler, maximizing flexibility and composition.
Use the adapter for functions. Use structs for state.