{} The Go Reference

Http · Web · Beginner

HTTP Server

Serving HTTP in net/http — handlers and HandlerFunc, the ResponseWriter and Request, the Go 1.22 method+pattern ServeMux with PathValue, decoding request bodies, and a production-shaped http.Server with timeouts.

Http Beginner ⏱ 6 min read Complete

🏪 Analogy

An HTTP server is a shop counter. A handler is the clerk who serves one customer (request) and hands back a receipt (response). The mux is the greeter who reads the sign — /orders, /users — and points each customer to the right clerk. Go hires a fresh clerk (goroutine) per customer, so the queue never backs up.

Handlers all the way down

The whole server is built on one interface — http.Handler — with a single method ServeHTTP(w, r). http.HandlerFunc lets a plain function satisfy it, which is why you can hand a function to HandleFunc:

graph LR
REQ["*http.Request"] --> MUX["ServeMux<br/>(route by method + path)"]
MUX --> H["Handler.ServeHTTP(w, r)"]
H --> W["http.ResponseWriter"]
W --> RES["status + headers + body"]

Run a handler in-process

You don’t need a network to test handler logic — httptest drives a handler directly with a recorder. This is exactly how you’d unit-test it, and it runs anywhere (the go.dev sandbox has no real network):

server.go — editable & runnable
package main

import (
"fmt"
"net/http"
"net/http/httptest"
)

// a handler reads the request and writes the response
func greet(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
	name = "world"
}
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "hello, %s", name)
}

func main() {
// httptest runs the handler IN-PROCESS: no socket, no network, safe anywhere
req := httptest.NewRequest("GET", "/greet?name=Ada", nil)
rec := httptest.NewRecorder()

greet(rec, req) // call the handler like any function

res := rec.Result()
fmt.Println("status:", res.StatusCode)                  // 200
fmt.Println("type:  ", res.Header.Get("Content-Type"))  // text/plain
fmt.Println("body:  ", rec.Body.String())               // hello, Ada
}

Routing with ServeMux

Since Go 1.22, the standard http.ServeMux matches on method and path pattern and supports path wildcards — so common routing no longer needs a third-party router. Register "GET /users/{id}" and read the wildcard with r.PathValue("id"):

mux.go — editable & runnable
package main

import (
"fmt"
"net/http"
"net/http/httptest"
)

func main() {
mux := http.NewServeMux()

// method + path pattern with a {id} wildcard (Go 1.22+)
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "get user %s", r.PathValue("id"))
})
mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusCreated)
	fmt.Fprint(w, "created")
})

// drive the mux in-process with a recorder
for _, tc := range []struct{ method, path string }{
	{"GET", "/users/42"},
	{"POST", "/users"},
	{"DELETE", "/users/42"}, // no route → 405 Method Not Allowed
} {
	rec := httptest.NewRecorder()
	mux.ServeHTTP(rec, httptest.NewRequest(tc.method, tc.path, nil))
	fmt.Printf("%s %-10s -> %d %q\n", tc.method, tc.path, rec.Code, rec.Body.String())
}
}

Reading the request body

For a JSON API, decode straight from r.Body with a json.Decoder, check the error, and validate. This handler echoes a decoded payload back as JSON — driven in-process by passing a body to httptest.NewRequest:

json.go — editable & runnable
package main

import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
)

type CreateUser struct {
Name string "json:\"name\""
Age  int    "json:\"age\""
}

func createUser(w http.ResponseWriter, r *http.Request) {
var in CreateUser
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
	http.Error(w, "bad json", http.StatusBadRequest)
	return
}
if in.Name == "" {
	http.Error(w, "name required", http.StatusUnprocessableEntity)
	return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(map[string]any{"id": 1, "name": in.Name})
}

func main() {
body := strings.NewReader("{\"name\":\"Ada\",\"age\":36}")
req := httptest.NewRequest("POST", "/users", body)
rec := httptest.NewRecorder()

createUser(rec, req)
fmt.Println("status:", rec.Code)        // 201
fmt.Print("body:   ", rec.Body.String()) // {"id":1,"name":"Ada"}
}

The ResponseWriter, in order

Write the response in the right sequence — headers, then status, then body:

  1. w.Header().Set(...) — set any headers first.
  2. w.WriteHeader(code) — send the status line (optional; defaults to 200 on first Write).
  3. w.Write(...) / fmt.Fprintf(w, ...) — the body.

Once you’ve written the body you can’t change the status or headers — they’re already on the wire. Use http.Error(w, msg, code) for a clean error response.

A production server

Bare http.ListenAndServe(addr, nil) has no timeouts, so a slow client can hold a connection open forever (the Slowloris attack). In production, build an explicit http.Server:

srv := &http.Server{
	Addr:              ":8080",
	Handler:           mux,
	ReadHeaderTimeout: 5 * time.Second,  // cap slow-header attacks
	ReadTimeout:       10 * time.Second,
	WriteTimeout:      30 * time.Second,
	IdleTimeout:       60 * time.Second,
}
log.Fatal(srv.ListenAndServe()) // run locally — needs a real network

Shut it down cleanly with srv.Shutdown(ctx) — see graceful shutdown.

Reference

TaskAPI
Define a handlerfunc(w http.ResponseWriter, r *http.Request)
Adapt a func to Handlerhttp.HandlerFunc
Route by method+pathmux.HandleFunc("GET /users/{id}", h)
Read a path wildcardr.PathValue("id")
Read a query paramr.URL.Query().Get("q")
Read a headerr.Header.Get("Authorization")
Decode a JSON bodyjson.NewDecoder(r.Body).Decode(&v)
Cap body sizehttp.MaxBytesReader(w, r.Body, n)
Set status / errorw.WriteHeader(code) / http.Error(w, msg, code)
Production server&http.Server{...timeouts...}

⚠️ Handlers run concurrently; write headers before the body

Every request gets its own goroutine, so any shared state a handler touches must be synchronized (a sync.Mutex or a channel) — the race detector will catch slips. The write order is strict: calling WriteHeader (or the first Write) freezes the status and headers, so set them first. And always give the server timeouts and bound request bodies — the defaults protect you from nothing.

See also

Next: calling other services — the HTTP client.

Check your understanding

Score: 0 / 5

1. What is an http.Handler?

Handler is a one-method interface: ServeHTTP(ResponseWriter, *Request). http.HandlerFunc adapts a plain func to it, which is why you can pass functions to HandleFunc.

2. How does net/http handle concurrent requests?

http.Server.Serve accepts connections and runs every request handler in its own goroutine. So handlers must be safe for concurrent use — guard shared state with a mutex or a channel.

3. Why prefer a configured http.Server over http.ListenAndServe(addr, nil) in production?

The default server has no timeouts, so a slow client can hold a connection forever (Slowloris). Construct an http.Server with explicit timeouts and your own mux for real deployments.

4. In Go 1.22+, how do you register a handler for GET requests to /users/{id} and read the id?

Go 1.22's ServeMux understands method+pattern strings like "GET /users/{id}" and exposes wildcards via r.PathValue("id"). Method-based matching and path parameters are now built in — no external router needed for common cases.

5. What's the safe way to decode a JSON request body in a handler?

Decode straight from r.Body with a Decoder, always check the error (malformed JSON, wrong types), and validate the result. Wrap the body in http.MaxBytesReader to reject oversized payloads, and set a Content-Type check for strictness.

Comments

Sign in with GitHub to join the discussion.