HTTP Server
HandleFunc ยท ServeMux ยท http.Server Request lifecycle:
Client โ net/http listener โ ServeMux (routing) โ Handler func
โ
ResponseWriter + *Request
โ
w.Write / w.Header / w.WriteHeader
Minimal HTTP server
HandleFunc
package main import ( "fmt" "net/http" ) func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }) http.ListenAndServe(":8080", nil) }
Production-ready http.Server
http.Server
srv := &http.Server{ Addr: ":8080", Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, } if err := srv.ListenAndServe(); err != http.ErrServerClosed { log.Fatalf("server: %v", err) }
Always set timeouts on
http.Server. The default zero values mean no timeout โ a slow or malicious client can hold a connection open forever, exhausting goroutines and file descriptors.Routing
ServeMux ยท method ยท path params
ServeMux โ Go 1.22+ enhanced patterns
ServeMux
mux := http.NewServeMux() // Go 1.22+: method and path-param patterns built in mux.HandleFunc("GET /users", listUsers) mux.HandleFunc("POST /users", createUser) mux.HandleFunc("GET /users/{id}", getUser) mux.HandleFunc("DELETE /users/{id}", deleteUser) // Extract path parameter inside the handler func getUser(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") // Go 1.22+ fmt.Fprintf(w, "user: %s", id) } // Trailing slash matches subtree: "/static/" matches all under /static/ mux.Handle("/static/", http.FileServer(http.Dir("./public")))
Reading request data
Request
func handler(w http.ResponseWriter, r *http.Request) { // Query params: /search?q=go&limit=10 q := r.URL.Query().Get("q") // Headers auth := r.Header.Get("Authorization") // JSON body var body Payload json.NewDecoder(r.Body).Decode(&body) // Form data (call ParseForm first) r.ParseForm() name := r.Form.Get("name") _ = q; _ = auth; _ = name }
Writing responses
ResponseWriter
func jsonResponse(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } // Must set headers BEFORE WriteHeader โ headers are sent // when WriteHeader is called and cannot be changed after // Convenience wrappers http.Error(w, "not found", http.StatusNotFound) http.Redirect(w, r, "/login", http.StatusSeeOther)
Middleware
handler wrapping ยท chain ยท contextGo middleware is just a function that takes an
http.Handler and returns an http.Handler. Wrap handlers to add logging, authentication, timeouts, or request IDs โ they compose with simple function calls.
Middleware pattern
Handler โ Handler
type Middleware func(http.Handler) http.Handler func Logger(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)) }) } func RequireAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization") if !validToken(token) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } next.ServeHTTP(w, r) }) } // Compose: outer runs first, inner last handler := Logger(RequireAuth(mux))
Passing values through context
context.WithValue
type contextKey string const userIDKey contextKey = "userID" // Middleware: extract user ID and store in context func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userID := extractUserID(r) ctx := context.WithValue(r.Context(), userIDKey, userID) next.ServeHTTP(w, r.WithContext(ctx)) }) } // Handler: retrieve value from context func profileHandler(w http.ResponseWriter, r *http.Request) { uid, _ := r.Context().Value(userIDKey).(string) fmt.Fprintf(w, "hello user %s", uid) }
HTTP Client
http.Get ยท http.Post ยท custom client
Simple GET and POST
http.Get
// GET resp, err := http.Get("https://api.example.com/data") if err != nil { return err } defer resp.Body.Close() var result Data json.NewDecoder(resp.Body).Decode(&result) // POST JSON body, _ := json.Marshal(payload) resp, err = http.Post( "https://api.example.com/items", "application/json", bytes.NewReader(body), )
Custom http.Client with timeout
http.Client
// Never use http.DefaultClient in production โ // it has no timeout and will hang indefinitely client := &http.Client{ Timeout: 10 * time.Second, } resp, err := client.Get("https://api.example.com/data") if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status: %d", resp.StatusCode) }
Building requests with custom headers
http.NewRequest
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/data", nil) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Accept", "application/json") resp, err := client.Do(req) defer resp.Body.Close() // Always drain + close body to allow connection reuse io.Copy(io.Discard, resp.Body)
Always read and close the response body, even when you don't need the content. If you only call
resp.Body.Close() without draining it first, the underlying TCP connection cannot be reused โ use io.Copy(io.Discard, resp.Body) before closing.Templates
html/template ยท text/template ยท ExecuteUse
html/template (not text/template) for anything rendered in a browser โ it automatically escapes values to prevent XSS. The two packages have identical APIs; only import differs.
Parse and execute a template
template.Execute
import "html/template" const tmplStr = ` <html><body> <h1>Hello, {{.Name}}!</h1> {{if .Admin}}<p>Admin panel</p>{{end}} <ul> {{range .Items}}<li>{{.}}</li>{{end}} </ul> </body></html>` tmpl := template.Must(template.New("page").Parse(tmplStr)) data := struct { Name string Admin bool Items []string }{"Alice", true, []string{"Go", "Rust"}} tmpl.Execute(w, data) // w is http.ResponseWriter
Parse files & named templates
ParseFiles
// Parse all templates at startup, not per request var tmpl = template.Must( template.ParseGlob("templates/*.html"), ) func homeHandler(w http.ResponseWriter, r *http.Request) { if err := tmpl.ExecuteTemplate(w, "home.html", data); err != nil { http.Error(w, "template error", 500) } }
Template syntax cheatsheet
Syntax
// Access field: {{.Name}} // Conditional: {{if .IsAdmin}} ... {{end}} // Else: {{if .X}} ... {{else}} ... {{end}} // Loop slice: {{range .Items}} {{.}} {{end}} // Loop with index: {{range $i, $v := .Items}} // Define block: {{define "header"}} ... {{end}} // Include block: {{template "header" .}} // Pipeline / function: {{.Name | printf "%q"}} // Variable: {{$x := .Name}} then {{$x}}
database/sql
Open ยท Query ยท Exec ยท Scan ยท Txdatabase/sql is a generic interface โ you need a driver package too (e.g. github.com/lib/pq for Postgres, github.com/mattn/go-sqlite3 for SQLite). Import the driver with a blank import to register it.
Open, query, and scan rows
sql.DB
import ( "database/sql" _ "github.com/lib/pq" // registers the postgres driver ) db, err := sql.Open("postgres", "postgres://user:pass@localhost/mydb") if err != nil { return err } defer db.Close() // sql.Open does not connect โ ping to verify if err := db.Ping(); err != nil { return err } // Query multiple rows rows, err := db.QueryContext(ctx, "SELECT id, name FROM users WHERE active = $1", true) if err != nil { return err } defer rows.Close() for rows.Next() { var id int var name string if err := rows.Scan(&id, &name); err != nil { return err } } return rows.Err() // check for iteration error
QueryRow โ single result
QueryRow
var name string err := db.QueryRowContext(ctx, "SELECT name FROM users WHERE id = $1", id, ).Scan(&name) if errors.Is(err, sql.ErrNoRows) { // not found } else if err != nil { return err }
Exec & transactions
Tx
tx, err := db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() // no-op if already committed _, err = tx.ExecContext(ctx, "INSERT INTO logs (msg) VALUES ($1)", msg) if err != nil { return err } return tx.Commit()
Always use the
Context variants (QueryContext, ExecContext, BeginTx) so database calls respect the request's deadline and can be cancelled when the client disconnects. Set db.SetMaxOpenConns and db.SetMaxIdleConns to control the connection pool.Graceful Shutdown
signal.NotifyContext ยท Shutdown
Catch SIGTERM / SIGINT and drain in-flight requests
Shutdown
srv := &http.Server{Addr: ":8080", Handler: mux} // Start server in background go func() { if err := srv.ListenAndServe(); err != http.ErrServerClosed { log.Fatalf("listen: %v", err) } }() // Block until SIGTERM or SIGINT ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer stop() <-ctx.Done() // Give in-flight requests up to 30s to finish shutCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := srv.Shutdown(shutCtx); err != nil { log.Printf("shutdown: %v", err) }
Quick Reference
Cheat-sheet| Concept | Call | Notes |
|---|---|---|
| Register handler | http.HandleFunc(pattern, fn) | Uses DefaultServeMux |
| Custom mux | mux := http.NewServeMux() | Preferred over DefaultServeMux |
| Method+path pattern | "GET /users/{id}" | Go 1.22+; use r.PathValue("id") |
| Start server | http.ListenAndServe(addr, mux) | Blocks; returns only on error |
| With timeouts | http.Server{ReadTimeout: โฆ} | Always set in production |
| Query params | r.URL.Query().Get("key") | Returns "" if absent |
| Request header | r.Header.Get("Authorization") | Canonical header name |
| Set response header | w.Header().Set("Content-Type", โฆ) | Must be before WriteHeader |
| Write status code | w.WriteHeader(http.StatusCreated) | Sends headers; can only call once |
| JSON response | json.NewEncoder(w).Encode(v) | Set Content-Type first |
| Error response | http.Error(w, msg, code) | Sets Content-Type text/plain |
| Redirect | http.Redirect(w, r, url, 302) | Use http.StatusSeeOther for POSTโGET |
| Middleware | func(http.Handler) http.Handler | Wrap and call next.ServeHTTP |
| Context value | r.WithContext(ctx) | Pass values through middleware chain |
| Simple GET | http.Get(url) | Uses DefaultClient โ no timeout! |
| Custom client | http.Client{Timeout: 10s} | Always set Timeout |
| Build request | http.NewRequestWithContext(ctx, โฆ) | Use for custom headers/context |
| Drain body | io.Copy(io.Discard, resp.Body) | Enables connection reuse |
| HTML template | template.Must(template.ParseGlob(โฆ)) | Parse at startup, not per request |
| Render template | tmpl.ExecuteTemplate(w, name, data) | auto-escapes for XSS prevention |
| DB connection | sql.Open(driver, dsn) | Lazy โ call Ping() to verify |
| Query rows | db.QueryContext(ctx, sql, argsโฆ) | defer rows.Close(); check rows.Err() |
| Single row | db.QueryRowContext(ctx, sql, โฆ).Scan() | Check sql.ErrNoRows |
| Execute SQL | db.ExecContext(ctx, sql, argsโฆ) | INSERT/UPDATE/DELETE |
| Transaction | db.BeginTx(ctx, nil) | defer tx.Rollback(); tx.Commit() |
| Graceful shutdown | srv.Shutdown(ctx) | Drains in-flight; use with signal.NotifyContext |