Go Web Development
Go's net/http standard library is production-ready. Most projects don't need a framework.
Handler Pattern
Everything revolves around one interface:
type Handler interface {
ServeHTTP(http.ResponseWriter, *http.Request)
}
Two ways to implement:
// 1. Implement the interface on a struct
type HealthHandler struct{}
func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
}
// 2. Use http.HandlerFunc adapter (most common)
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok"}`))
}
mux.HandleFunc("GET /health", healthHandler)
Routing Decision Table
| Need |
Approach |
| Simple REST API (Go 1.22+) |
http.NewServeMux with method+pattern: "GET /api/users/{id}" |
| Complex routing, groups, middleware chaining |
chi router |
| Pre-Go 1.22 projects needing path params |
gorilla/mux or chi |
| gRPC services |
google.golang.org/grpc |
Go 1.22+ Enhanced ServeMux
mux := http.NewServeMux()
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
// Extract path params
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
// ...
}
Middleware Pattern
Middleware wraps a handler and returns a handler:
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
slog.Info("request", "method", r.Method, "path", r.URL.Path, "duration", time.Since(start))
})
}
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if !isValid(token) {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Chain: logging → auth → handler
handler := logging(auth(mux))
JSON Quick Reference
| Operation |
Code |
| Struct → JSON bytes |
json.Marshal(v) |
| JSON bytes → struct |
json.Unmarshal(data, &v) |
| Write JSON to response |
json.NewEncoder(w).Encode(v) |
| Read JSON from request |
json.NewDecoder(r.Body).Decode(&v) |
| Omit zero-value field |
Tag: json:",omitempty" |
| Rename field |
Tag: json:"field_name" |
| Ignore field |
Tag: json:"-" |
func createUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON", http.StatusBadRequest)
return
}
user, err := svc.CreateUser(r.Context(), req)
if err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
Anti-patterns
| Anti-pattern |
Problem |
Fix |
Not closing resp.Body on HTTP client calls |
Resource leak |
defer resp.Body.Close() after nil-error check |
Writing after http.Error |
Double write, corrupted response |
return after http.Error(w, ...) |
| Panic in handlers without recover |
Server crashes on one bad request |
Add recover middleware |
| Global default mux |
No middleware control, test pollution |
Create http.NewServeMux() explicitly |
| Unbounded request body |
DoS vector |
http.MaxBytesReader(w, r.Body, maxBytes) |
Read On Demand
| Read When |
File |
| ServeMux patterns, middleware composition, graceful shutdown, timeouts |
HTTP Server |
| encoding/json details, XML, html/template patterns |
JSON & Templates |
Benchmark
Scenario: .benchmarks/scenarios/golang-web-001-handler-audit.md · Run: 2026-08-31 · Log: .benchmarks/runs/2026-08-31/golang-web-001-handler-audit.json
| Model |
Without |
With |
Delta |
| claude-opus-4-8 |
100% |
100% |
+0% |
| claude-sonnet-4-6 |
100% |
100% |
+0% |
| claude-haiku-4-5 |
100% |
100% |
+0% |
NEUTRAL (run 2026-08-31). All models 100% with and without — handler-audit criteria at ceiling. Gate per .agents/skills/skill-optimizer/rules/release-gates.md.
1---2name: web3description: Go web development — HTTP server, handlers, middleware, routing, JSON encoding, XML, templates. TRIGGER when: user asks about Go HTTP server, net/http, Go handler, Go middleware, Go routing, Go JSON, encoding/json, json.Marshal, json.Unmarshal, Go XML, Go templates, html/template, Go REST API, Go web service, http.ListenAndServe, http.HandlerFunc, Go request handling, Go response writer, Go ServeMux, Go router, chi router, Go graceful shutdown, Go HTTP client. DO NOT USE when: non-web JSON/struct marshaling or general Go with no HTTP server/handler/router → use the relevant `golang` sub-skill; non-Go web work.4---56# Go Web Development78Go's `net/http` standard library is production-ready. Most projects don't need a framework.910---1112## Handler Pattern1314Everything revolves around one interface:1516```go17type Handler interface {18 ServeHTTP(http.ResponseWriter, *http.Request)19}20```2122Two ways to implement:2324```go25// 1. Implement the interface on a struct26type HealthHandler struct{}2728func (h *HealthHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {29 w.WriteHeader(http.StatusOK)30 w.Write([]byte(`{"status":"ok"}`))31}3233// 2. Use http.HandlerFunc adapter (most common)34func healthHandler(w http.ResponseWriter, r *http.Request) {35 w.WriteHeader(http.StatusOK)36 w.Write([]byte(`{"status":"ok"}`))37}3839mux.HandleFunc("GET /health", healthHandler)40```4142---4344## Routing Decision Table4546| Need | Approach |47| -------------------------------------------- | --------------------------------------------------------------- |48| Simple REST API (Go 1.22+) | `http.NewServeMux` with method+pattern: `"GET /api/users/{id}"` |49| Complex routing, groups, middleware chaining | `chi` router |50| Pre-Go 1.22 projects needing path params | `gorilla/mux` or `chi` |51| gRPC services | `google.golang.org/grpc` |5253### Go 1.22+ Enhanced ServeMux5455```go56mux := http.NewServeMux()5758mux.HandleFunc("GET /api/users", listUsers)59mux.HandleFunc("GET /api/users/{id}", getUser)60mux.HandleFunc("POST /api/users", createUser)61mux.HandleFunc("DELETE /api/users/{id}", deleteUser)6263// Extract path params64func getUser(w http.ResponseWriter, r *http.Request) {65 id := r.PathValue("id")66 // ...67}68```6970---7172## Middleware Pattern7374Middleware wraps a handler and returns a handler:7576```go77func logging(next http.Handler) http.Handler {78 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {79 start := time.Now()80 next.ServeHTTP(w, r)81 slog.Info("request", "method", r.Method, "path", r.URL.Path, "duration", time.Since(start))82 })83}8485func auth(next http.Handler) http.Handler {86 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {87 token := r.Header.Get("Authorization")88 if !isValid(token) {89 http.Error(w, "unauthorized", http.StatusUnauthorized)90 return91 }92 next.ServeHTTP(w, r)93 })94}9596// Chain: logging → auth → handler97handler := logging(auth(mux))98```99100---101102## JSON Quick Reference103104| Operation | Code |105| ---------------------- | ------------------------------------ |106| Struct → JSON bytes | `json.Marshal(v)` |107| JSON bytes → struct | `json.Unmarshal(data, &v)` |108| Write JSON to response | `json.NewEncoder(w).Encode(v)` |109| Read JSON from request | `json.NewDecoder(r.Body).Decode(&v)` |110| Omit zero-value field | Tag: `json:",omitempty"` |111| Rename field | Tag: `json:"field_name"` |112| Ignore field | Tag: `json:"-"` |113114```go115func createUser(w http.ResponseWriter, r *http.Request) {116 var req CreateUserRequest117 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {118 http.Error(w, "invalid JSON", http.StatusBadRequest)119 return120 }121122 user, err := svc.CreateUser(r.Context(), req)123 if err != nil {124 http.Error(w, "internal error", http.StatusInternalServerError)125 return126 }127128 w.Header().Set("Content-Type", "application/json")129 w.WriteHeader(http.StatusCreated)130 json.NewEncoder(w).Encode(user)131}132```133134---135136## Anti-patterns137138| Anti-pattern | Problem | Fix |139| -------------------------------------------- | ------------------------------------- | ----------------------------------------------- |140| Not closing `resp.Body` on HTTP client calls | Resource leak | `defer resp.Body.Close()` after nil-error check |141| Writing after `http.Error` | Double write, corrupted response | `return` after `http.Error(w, ...)` |142| Panic in handlers without recover | Server crashes on one bad request | Add recover middleware |143| Global default mux | No middleware control, test pollution | Create `http.NewServeMux()` explicitly |144| Unbounded request body | DoS vector | `http.MaxBytesReader(w, r.Body, maxBytes)` |145146---147148## Read On Demand149150| Read When | File |151| ---------------------------------------------------------------------- | ------------------------------------------------ |152| ServeMux patterns, middleware composition, graceful shutdown, timeouts | [HTTP Server](references/http-server.md) |153| encoding/json details, XML, html/template patterns | [JSON & Templates](references/json-templates.md) |154155---156157## Benchmark158159Scenario: `.benchmarks/scenarios/golang-web-001-handler-audit.md` · Run: 2026-08-31 · Log: `.benchmarks/runs/2026-08-31/golang-web-001-handler-audit.json`160161| Model | Without | With | Delta |162| ----------------- | ------- | ---- | ----- |163| claude-opus-4-8 | 100% | 100% | +0% |164| claude-sonnet-4-6 | 100% | 100% | +0% |165| claude-haiku-4-5 | 100% | 100% | +0% |166167> **NEUTRAL (run 2026-08-31)**. All models 100% with and without — handler-audit criteria at ceiling. Gate per `.agents/skills/skill-optimizer/rules/release-gates.md`.