Irbisa · Cheatsheet
HTTP Services & REST APIs
net/http, ServeMux patterns, middleware, timeouts, graceful shutdown
How do you write a minimal HTTP server with
net/http, and what dohttp.Handler,http.HandlerFuncandhttp.ServeMuxeach do?type itemsHandler struct { store *Store} // itemsHandler is an http.Handler because it has ServeHTTP.func (h itemsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {…How do you read a path parameter, a query parameter, a header and a JSON body in a handler, and send back a JSON response?
type renameReq struct { Name string `json:"name"`} // Registered as mux.HandleFunc("PUT /items/{id}", renameItem).func renameItem(w http.ResponseWriter, r *http.Request) {…This handler should answer
201 Createdwith a JSON body. What status andContent-Typedoes the client actually get, and why?func createItem(w http.ResponseWriter, r *http.Request) { item := Item{ID: 42, Name: "lamp"} json.NewEncoder(w).Encode(item) w.WriteHeader(http.StatusCreated) w.Header().Set("Content-Type", "application/json")}Which HTTP methods are safe and idempotent, how does
PUTdiffer fromPATCH, and which status codes do you return for common outcomes?What did Go 1.22 add to
http.ServeMuxrouting, and how does the mux pick a handler when two patterns match?func routes(s *Server) *http.ServeMux { mux := http.NewServeMux() mux.HandleFunc("GET /items", s.listItems) mux.HandleFunc("POST /items", s.createItem) mux.HandleFunc("GET /items/new", s.newItemForm) // beats {id}: more specific mux.HandleFunc("GET /items/{id}", s.getItem)…How does
net/httpserve many requests at once, and what does that mean for state that handlers share?What is middleware in Go, and how would you write one that logs method, path, status and duration and passes a request ID down the chain?
type ctxKey struct{} func RequestID(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { id := r.Header.Get("X-Request-Id") // set by our own proxy if id == "" {…In what order does a middleware chain like
a(b(c(mux)))run, and where do request ID, logging, recovery, CORS and auth belong in it?// Chain(a, b, c)(h) == a(b(c(h))): a runs first on the way in.func Chain(mws ...func(http.Handler) http.Handler) func(http.Handler) http.Handler { return func(h http.Handler) http.Handler { for i := len(mws) - 1; i >= 0; i-- { h = mws[i](h) }…Find the bug: after a panic in an export handler, some clients get a
200with a cut-off JSON body ending ininternal error.func recoverPanics(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if v := recover(); v != nil { slog.Error("panic", "err", v) http.Error(w, "internal error", http.StatusInternalServerError) } }() next.ServeHTTP(w, r) })}func export(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, `{"rows":[1,2,`) var m map[string]int m["x"] = 1 // panics mid-response}How do you decode a JSON request body safely, and what is wrong with
body, _ := io.ReadAll(r.Body)followed byjson.Unmarshal?func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) error { r.Body = http.MaxBytesReader(w, r.Body, 1<<20) // 1 MiB dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() if err := dec.Decode(dst); err != nil {…How should a JSON API report errors and field-level validation failures, and why is
http.Error(w, err.Error(), 500)the wrong tool for it?// RFC 9457 problem details.type Problem struct { Title string `json:"title"` Status int `json:"status"` Detail string `json:"detail,omitempty"` Errors map[string]string `json:"errors,omitempty"` // extension: field -> reason…How would you structure a Go REST service into handler, service and repository layers, and how do the handlers get their dependencies?
// Declared where it is used; *postgres.UserRepo satisfies it implicitly.type UserRepo interface { ByID(ctx context.Context, id int64) (User, error) Create(ctx context.Context, u User) (int64, error)}…How would you design a REST API for a resource such as blog posts: URLs, methods, pagination and versioning?
For a new service, would you use the standard
net/httpmux, chi, gin, echo or fiber, and what does each choice cost?How would you implement rate limiting as HTTP middleware, and what should the client get back when it hits the limit?
type RateLimiter struct { mu sync.Mutex limiters map[string]*rate.Limiter} func NewRateLimiter() *RateLimiter {…Which timeouts should a production
http.Serverset, and why ishttp.ListenAndServe(":8080", mux)unsafe on a public port?func newServer(h http.Handler, logger *slog.Logger) *http.Server { return &http.Server{ Addr: ":8080", Handler: http.TimeoutHandler(h, 10*time.Second, "request timed out"), ReadHeaderTimeout: 5 * time.Second, // the Slowloris fix ReadTimeout: 30 * time.Second, // headers + body…How do you shut down a Go HTTP server gracefully on
SIGTERMwithout dropping in-flight requests?func run(handler http.Handler, db *sql.DB) error { ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() srv := &http.Server{Addr: ":8080", Handler: handler, ReadHeaderTimeout: 5 * time.Second}…This handler starts a slow export in the background and answers
202 Accepted. What is wrong with it?func (s *Server) startExport(w http.ResponseWriter, r *http.Request) { go func() { data, err := s.buildExport(r.Context()) if err != nil { log.Println(err) return } w.Write(data) }() w.WriteHeader(http.StatusAccepted)}How should a service create its
http.Client, and how do you give each outbound call its own deadline?// Built once, at startup, and shared.func newUpstreamClient() *http.Client { tr := http.DefaultTransport.(*http.Transport).Clone() tr.MaxIdleConnsPerHost = 100 return &http.Client{Transport: tr, Timeout: 5 * time.Second} // backstop}…How do you stream Server-Sent Events from a Go handler, and why might the events arrive all at once or the stream die at
WriteTimeout?func (s *Server) events(w http.ResponseWriter, r *http.Request) { rc := http.NewResponseController(w) w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("X-Accel-Buffering", "no") // nginx: do not buffer this response w.WriteHeader(http.StatusOK)…This client reuses one
http.Client, yet when the upstream answers404to many lookups, almost every call opens a new TCP and TLS connection. Why?func fetchUser(ctx context.Context, id string) (*User, error) { req, _ := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+"/users/"+id, nil) resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("upstream: %s", resp.Status) } var u User return &u, json.NewDecoder(resp.Body).Decode(&u)}How do you tune
http.Transportfor thousands of requests per second to one upstream, and how does connection reuse differ in HTTP/1.1 and HTTP/2?// wrong: a bare Transport - HTTP/1.1 only, no proxy, an idle pool of 2 per host// client := &http.Client{Transport: &http.Transport{TLSClientConfig: cfg}} func newHotClient(cfg *tls.Config) *http.Client { tr := http.DefaultTransport.(*http.Transport).Clone() tr.TLSClientConfig = cfg…How do you make
POST /chargessafe to retry, and which requests does Go'shttp.Transportretry on its own?func charge(ctx context.Context, c *http.Client, body []byte) (*http.Response, error) { key := rand.Text() // one key for the operation, reused by every attempt var lastErr error for attempt := range 4 { // A Request is single-use; bytes.NewReader sets GetBody so it can be replayed. req, err := http.NewRequestWithContext(ctx, http.MethodPost, chargesURL, bytes.NewReader(body))…