net/http
Build HTTP servers with Go's standard library — HandleFunc, ServeMux, the Handler interface, middleware, and request/response types.
- 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 http.Handler 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 middleware 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.You want to pass your database connection into an HTTP handler. What is the idiomatic way?
- 2.Using http.DefaultServeMux is recommended in production because it is pre-configured for performance.
- 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.