Go Cheatsheet
HTTP Servers
Use this Go reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Minimal Server
net/http is a production HTTP server in the standard library. No framework needed.
package main import ( "fmt" "log" "net/http" ) func main() { mux := http.NewServeMux() mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }) log.Fatal(http.ListenAndServe(":8080", mux)) }
ListenAndServe blocks and always returns a non-nil error. It sets no timeouts
at all, so a slow client can hold a connection forever. Use an explicit
&http.Server{} for anything real. Passing nil as the handler uses
http.DefaultServeMux, the package global that top-level http.HandleFunc
writes into. Avoid it, since any imported package can register routes on it.
Handler vs HandlerFunc
A handler is any type with one method. http.HandlerFunc adapts a plain function
to it.
type Handler interface { ServeHTTP(ResponseWriter, *Request) } func hello(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello")) } mux.Handle("/hello", http.HandlerFunc(hello)) mux.HandleFunc("/hello", hello) // same thing, less typing // Dependencies go on a struct, methods become the handlers type api struct{ db *sql.DB } func (a *api) getItem(w http.ResponseWriter, r *http.Request) { row := a.db.QueryRow("SELECT name FROM items WHERE id = $1", r.PathValue("id")) _ = row } mux.HandleFunc("GET /items/{id}", a.getItem)
ServeMux Patterns (Go 1.22+)
Since Go 1.22 a pattern is [METHOD ][HOST]/[PATH]. All three parts are
optional, and the method must be followed by at least one space.
| Pattern | Matches |
|---|---|
/items/ | subtree: /items/, /items/1, /items/a/b |
/items | exactly /items, any method |
GET /items/{id} | one segment after /items/, GET and HEAD only |
POST /items | POST to /items only |
/items/{$} | exactly /items/, no subtree |
/{$} | the root path / only |
/ | every path (catch-all) |
/files/{path...} | rest of the path, slashes included |
api.example.com/v1/ | that host only |
mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "item %s\n", r.PathValue("id")) // "" if absent }) mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, r.PathValue("path")) // "a/b/c.txt" })
Rules that bite:
- Most specific pattern wins. /images/thumbs/ beats /images/, and
GET /items/{id} beats GET /items/. Two patterns that conflict with no
winner make Handle panic at registration, not at request time.
- A pattern with GET also matches HEAD. Any other method matches exactly.
- A pattern with no method matches every method, so a bare /items swallows
POST too. Register each method and let the mux answer 405 for the rest.
- Wildcards must be whole segments. /b_{bucket} is invalid and panics, and
{name...} is legal only at the end.
- A trailing slash acts as an anonymous {...} wildcard, so /items/ is a
subtree, not a literal path.
- Paths are unescaped per segment before matching, and .. or doubled slashes
get a 301 to the cleaned path.
- mux.Handler(r) returns the matched handler and its pattern, useful for
logging the route template instead of the raw path.
Reading the Request
// Query string: /search?q=go&tag=a&tag=b q := r.URL.Query() term := q.Get("q") // "" if absent tags := q["tag"] // []string{"a", "b"} page, _ := strconv.Atoi(q.Get("page")) r.URL.Path, r.Method, r.Host, r.RemoteAddr r.Header.Get("Authorization") r.Cookie("session") // (*http.Cookie, error) name := r.FormValue("name") // query + urlencoded body, parses for you r.PostFormValue("name") // body only
JSON bodies, with a size cap so one huge upload cannot exhaust memory:
type createReq struct { Name string `json:"name"` Email string `json:"email"` } func create(w http.ResponseWriter, r *http.Request) { r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() var in createReq if err := dec.Decode(&in); err != nil { http.Error(w, "bad request body", http.StatusBadRequest) return } }
File uploads:
if err := r.ParseMultipartForm(32 << 20); err != nil { // 32 MiB in memory http.Error(w, "bad form", http.StatusBadRequest) return } file, hdr, err := r.FormFile("avatar") if err != nil { /* ... */ } defer file.Close() fmt.Println(hdr.Filename, hdr.Size)
r.Body is never nil on the server and the server closes it, so no defer
Close is needed here (it is required on the client side).
Writing the Response
Order matters. Headers, then status, then body.
w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") w.WriteHeader(http.StatusCreated) // 201 json.NewEncoder(w).Encode(map[string]any{"ok": true})
- Any
w.Header().SetafterWriteHeaderor the firstWriteis ignored. - The first
Writeimplicitly sends 200 OK, soWriteHeadermust come first. - A second
WriteHeaderlogs "superfluous response.WriteHeader call". - Always
returnafterhttp.Error, otherwise you write the body twice. - Content-Type is sniffed from the first 512 bytes when you do not set it.
http.Error(w, "not found", http.StatusNotFound) // plain text + status http.NotFound(w, r) // 404 shorthand http.Redirect(w, r, "/login", http.StatusSeeOther) // 303 http.SetCookie(w, &http.Cookie{ Name: "session", Value: tok, Path: "/", HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode, })
Marshal before writing, so an encoding failure cannot land inside a 200:
func writeJSON(w http.ResponseWriter, status int, v any) { buf, err := json.Marshal(v) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) w.Write(buf) }
Static Files
// Serve ./public at /static/ fs := http.FileServer(http.Dir("public")) mux.Handle("GET /static/", http.StripPrefix("/static/", fs)) // One file, with Range, If-Modified-Since and Content-Type handled mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "public/favicon.ico") })
Without StripPrefix the file server looks for public/static/... and 404s.
FileServer also lists any directory with no index.html, so prefer
http.FileServerFS(sub) over an embed.FS when you want a fixed asset set.
Middleware
Middleware is a function that wraps a handler. That is the whole pattern.
type middleware func(http.Handler) http.Handler func logging(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { start := time.Now() sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} next.ServeHTTP(sw, r) slog.Info("request", "method", r.Method, "path", r.URL.Path, "status", sw.status, "dur", time.Since(start)) }) } // ResponseWriter does not expose the status, so wrap it type statusWriter struct { http.ResponseWriter status int } func (s *statusWriter) WriteHeader(code int) { s.status = code s.ResponseWriter.WriteHeader(code) } func recoverPanic(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if rec := recover(); rec != nil { slog.Error("panic", "err", rec, "stack", string(debug.Stack())) w.Header().Set("Connection", "close") http.Error(w, "internal error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) }) } // Chain, outermost first func chain(h http.Handler, ms ...middleware) http.Handler { for i := len(ms) - 1; i >= 0; i-- { h = ms[i](h) } return h } handler := chain(mux, recoverPanic, logging)
The server survives a handler panic on its own, but it kills that connection and dumps a raw stack, so a recover middleware that returns a clean 500 still earns its place. It cannot catch a panic in a goroutine the handler started. Wrapping the mux covers every route including 404s, wrapping one handler covers that route.
Server Timeouts and Graceful Shutdown
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second, // Slowloris defense, always set it
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}| Field | Covers | Note |
|---|---|---|
ReadHeaderTimeout | request line + headers | the non-negotiable one |
ReadTimeout | headers + body | too small breaks large uploads |
WriteTimeout | writing the response | too small breaks SSE and streaming |
IdleTimeout | keep-alive gap | falls back to ReadTimeout if zero |
For streaming, leave the read and write timeouts generous and put per-request
deadlines on r.Context() instead, or wrap with http.TimeoutHandler(h, d, msg).
TLS is one call: srv.ListenAndServeTLS("cert.pem", "key.pem").
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() go func() { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { log.Fatalf("listen: %v", err) } }() <-ctx.Done() stop() // restore default signal handling shutCtx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() if err := srv.Shutdown(shutCtx); err != nil { log.Printf("forced close: %v", err) srv.Close() }
ListenAndServe returns http.ErrServerClosed on a clean shutdown, which is not
a failure. Shutdown drains idle connections and waits for active requests, then
returns the context error if the grace period expires. It does not wait for
hijacked connections, so track those yourself. srv.Close() cuts everything now.
Request Context
r.Context() is cancelled when the client disconnects or the handler returns, so
passing it down cancels DB queries and outbound calls for free.
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) defer cancel() rows, err := db.QueryContext(ctx, "SELECT id FROM items") switch { case errors.Is(err, context.Canceled): return // client is gone, nobody to answer case errors.Is(err, context.DeadlineExceeded): http.Error(w, "timeout", http.StatusGatewayTimeout) return case err != nil: http.Error(w, "db error", http.StatusInternalServerError) return } defer rows.Close()
Request-scoped values ride the context under an unexported key type, so no other package can collide:
type userKey struct{} func auth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { u, err := userFrom(r.Header.Get("Authorization")) if err != nil { http.Error(w, "unauthorized", http.StatusUnauthorized) return } ctx := context.WithValue(r.Context(), userKey{}, u) next.ServeHTTP(w, r.WithContext(ctx)) }) } func userOf(r *http.Request) (*User, bool) { u, ok := r.Context().Value(userKey{}).(*User) return u, ok }
Never hand r.Context() to a goroutine that outlives the handler, it is
cancelled the moment the handler returns. Use context.WithoutCancel(r.Context())
for genuine background work.
Streaming, Flusher, and SSE
http.ResponseWriter optionally implements more interfaces. Assert for them.
| Interface | Method | Use |
|---|---|---|
http.Flusher | Flush() | push buffered bytes to the client now |
http.Hijacker | Hijack() | take over the raw TCP conn (WebSockets) |
io.ReaderFrom | ReadFrom() | zero-copy file sends |
http.CloseNotifier | CloseNotify() | deprecated, use r.Context() |
func events(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "streaming unsupported", http.StatusInternalServerError) return } tick := time.NewTicker(time.Second) defer tick.Stop() for { select { case <-r.Context().Done(): return case t := <-tick.C: fmt.Fprintf(w, "data: %s\n\n", t.Format(time.RFC3339)) flusher.Flush() } } }
SSE needs WriteTimeout: 0, a blank line after every event, and response
buffering off in any reverse proxy in front. Flush behaves the same under HTTP/2.
Testing Handlers
httptest covers both directions and needs no open port.
func TestGetItem(t *testing.T) { req := httptest.NewRequest("GET", "/items/42", nil) rec := httptest.NewRecorder() routes().ServeHTTP(rec, req) // your real *http.ServeMux if rec.Code != http.StatusOK { t.Fatalf("status = %d, want 200", rec.Code) } // rec.Body is a *bytes.Buffer, rec.Result() is a real *http.Response } // A real server on a random port, for end-to-end tests srv := httptest.NewServer(routes()) defer srv.Close() resp, err := srv.Client().Get(srv.URL + "/items/42")
Because PathValue is filled in by the mux, always test through the mux, not
by calling the handler function directly. httptest.NewTLSServer plus
srv.Client() gives you HTTPS with the cert already trusted.
ServeMux Limits
Post-1.22 ServeMux does methods, wildcards and host matching, which is enough
for most services. What it lacks: regex or typed path constraints, per-group
middleware, route naming and URL building, and richer 405 handling. Reach for
chi (plain http.Handler, groups, middleware)
only when you need one of those, and never pick a framework that hides
http.Handler.