The net/http Mental Model
Here's a thing that catches people off guard when they come to Go from almost anywhere else: you often
don't reach for a web framework at all. The standard library ships a production-grade HTTP server, a
router, and everything you need to read a request and write a response — right there in net/http, no
go get required. This is the roots guide. Learn what's in here and Gin,
Echo, and chi stop being magic — they read as "net/http with some boilerplate removed."
📝 This is the Go parallel to WSGI & ASGI Explained — Python's foundation under Flask and Django. Same idea, different ecosystem: a standard shape that the server and your code agree on. This guide assumes you know basic HTTP (methods, status codes, headers — see HTTP, Explained) and Go itself (Go From Zero).
The whole architecture is three types
Before any code, get the picture in your head — the code will then just be names attached to ideas you
already hold. The entire net/http server model is three standard types, and they each do exactly
one job.
📝 http.Handler — an interface with a single method:
}
Anything that has a ServeHTTP(w, r) method is a handler. This is your request-handling code — the
part that looks at a request and writes a response. r is the incoming request (method, path, headers,
body); w is where you write the reply.
📝 http.HandlerFunc — an adapter that lets a plain function func(w, r) count as a Handler,
so you don't have to declare a type with a method every time you want to handle a request. (More on the
trick behind this below — it's the one piece of cleverness in the whole design.)
📝 http.ServeMux — the router (mux = "multiplexer"). It maps URL patterns like /ping or
/messages to the handler that should serve them. The neat part: a ServeMux is itself a Handler —
its ServeHTTP looks at the request path and forwards to the right registered handler.
📝 http.Server — ties an address (like :8080) to a handler and listens for connections.
It owns the accept-loop and the HTTP plumbing; when a request arrives it calls the handler's ServeHTTP.
Here's the mental model to carry through the whole guide, the sentence everything else hangs off of:
💡 The mux routes a request to a handler; the handler writes the response. That's it. The server runs the loop, the mux picks who handles each request, and the handler does the one interesting thing — decides what to send back.
flowchart LR
C[Client] -->|HTTP request| S[http.Server]
S -->|ServeHTTP| M[http.ServeMux]
M -->|matches path, forwards| H[your Handler]
H -->|writes to w| R[HTTP response]
R --> C
What just happened: the client sends a request; the server accepts it and calls the handler it was
given. That handler is usually the mux, which looks at the path and forwards to the specific handler
you registered for it. Your handler writes the response into w, and the server ships it back. Notice the
mux is just a handler that happens to delegate — that uniformity is why the whole thing composes so well.
The smallest server that does something
Now the picture in code. This is a complete, runnable Go program — a server that answers /ping with
pong.
package main
What just happened: line by line against the three types —
http.NewServeMux()creates the router. Empty for now, with no routes.mux.HandleFunc("/ping", ...)registers a route: "when a request comes in for/ping, run this function." The function is a plainfunc(w, r)—HandleFuncwraps it as aHandlerfor you (that's theHandlerFuncadapter doing its job behind the scenes).- Inside the handler,
fmt.Fprintln(w, "pong")writespongtow— theResponseWriter. Writing towis sending the response body. Noreturnof a value; you write, the client receives. http.ListenAndServe(":8080", mux)is the server: bind to port 8080 and listen, usingmuxas the top-level handler. This call blocks — it runs the accept-loop forever (until something goes wrong).
⚠️ http.ListenAndServe returns an error and we're ignoring it here. That's fine for a first sketch,
but real code checks it — log.Fatal(http.ListenAndServe(...)) is the usual one-liner. Phase 6 replaces
this convenience with an explicit http.Server so you can set timeouts and shut down cleanly. For now,
know that ListenAndServe is a shortcut that builds an http.Server for you under the hood. The mux is
the handler it serves.
Run it and hit it:
# in another terminal:
# pong
What just happened: go run main.go compiles and starts the program; it sits there blocked in
ListenAndServe, holding port 8080. curl opens a connection, sends GET /ping, the server routes it
through the mux to your function, your function writes pong, and curl prints it. You just ran a web
server with zero dependencies.
Why a plain function is allowed to be a Handler
Look back at that example: ServeMux wants Handlers (things with a ServeHTTP method), but you handed
it a bare function. How does a function satisfy an interface? This is http.HandlerFunc, and it's worth
understanding because it's the move every Go web framework copies.
http.HandlerFunc is a named function type defined in the standard library, roughly:
What just happened: HandlerFunc is a type whose underlying type is a function, and it has a
ServeHTTP method that calls the function it wraps. So when you convert a func(w, r) into a
HandlerFunc, it suddenly satisfies the Handler interface — ServeHTTP is defined, and all it does is
invoke your function. mux.HandleFunc(...) does that conversion for you; you could also write
mux.Handle("/ping", http.HandlerFunc(myFunc)) and get the same result. The adapter exists so you can
write handlers as ordinary functions instead of declaring a struct with a method every single time.
💡 This is the small, elegant trick at the heart of net/http: by making "a handler" an interface with
one method, anything can be a handler — a function (via HandlerFunc), a struct, the mux itself, or a
wrapper around another handler (that last one is middleware, in Phase 4). One interface, endless
composition.
Where the frameworks fit — and what we'll build
Here's the reveal that justifies the whole guide. When you eventually use Gin or Echo or chi, you're not escaping these types — you're sitting on top of them.
💡 Gin, Echo, and chi are conveniences over exactly Handler, HandlerFunc, and ServeMux. Their
routers are http.Handlers; you can mount a chi router as the handler in an http.Server, or wrap a Gin
engine and serve it the same way you served mux above. They add nicer routing, parameter binding, and
helpers — but the request still enters through a server, gets routed, and lands on something that writes a
response. The skeleton is the one you just met.
To make all of this concrete instead of abstract, the rest of the guide builds one small thing the whole way through: a messages service. The core data is deliberately tiny —
What just happened: nothing yet — that's just the shape of the data our API will serve. Over the next phases we'll route requests to it (Phase 2), read and write it as JSON (Phase 3), wrap it with middleware (Phase 4), and grow it into a full CRUD REST API with no framework at all (Phase 5). Every step is the same three types, doing their one job each.
Recap
- The entire
net/httpserver model is three types:http.Handler(your code),http.ServeMux(the router), andhttp.Server(listens on an address) — plus theHandlerFuncadapter. - A
Handleris anything with aServeHTTP(w, r)method. You write towto send the response;ris the incoming request. - The mental model, all the way down: the mux routes a request to a handler; the handler writes the response. The mux is itself a handler that delegates by path.
http.HandlerFunclets a plainfunc(w, r)satisfy theHandlerinterface — itsServeHTTPjust calls the function — so you write handlers as ordinary functions.http.ListenAndServe(":8080", mux)is a convenience that builds anhttp.Serverfor you and serves the mux as its handler. Phase 6 swaps in an explicithttp.Serverfor timeouts and graceful shutdown.- Gin/Echo/chi are conveniences over these same types — their routers are
http.Handlers. We'll build a messages service (Message{ID, Text}) on the bare standard library across the guide.
Quick check
Three questions on the ideas that have to stick before Phase 2:
[
{
"q": "What makes something an http.Handler in Go?",
"choices": [
"It has a ServeHTTP(w http.ResponseWriter, r *http.Request) method",
"It is registered with mux.HandleFunc",
"It imports the net/http package",
"It returns an (response, error) pair"
],
"answer": 0,
"explain": "http.Handler is an interface with exactly one method, ServeHTTP(w, r). Anything that defines that method satisfies the interface and can serve requests — a function (via HandlerFunc), a struct, even the ServeMux itself."
},
{
"q": "In one sentence, what is the net/http mental model?",
"choices": [
"The mux routes a request to a handler; the handler writes the response",
"The handler routes requests and the server writes the response",
"The client builds the response and the server validates it",
"The Server parses the body and the Handler manages the TCP socket"
],
"answer": 0,
"explain": "The ServeMux looks at the request path and forwards to the right handler; that handler writes the reply into w. The Server runs the accept-loop and calls the handler. Mux routes, handler writes — that's the whole architecture."
},
{
"q": "Why can you pass a plain func(w, r) where a Handler is expected?",
"choices": [
"http.HandlerFunc is a function type whose ServeHTTP method just calls the function, so it satisfies the Handler interface",
"Go automatically treats every function as an interface",
"ListenAndServe converts all functions into goroutines",
"ServeMux ignores the Handler interface entirely"
],
"answer": 0,
"explain": "http.HandlerFunc is a named type whose underlying type is func(w, r), and it defines ServeHTTP to call that function. Converting your function to a HandlerFunc gives it a ServeHTTP method, so it satisfies Handler. mux.HandleFunc does this conversion for you."
}
]
Guide overview · Phase 2: Handlers & Routing by Hand →
Check your understanding
1. What makes something an http.Handler in Go?
2. In one sentence, what is the net/http mental model?
3. Why can you pass a plain func(w, r) where a Handler is expected?