Go Web

HTTP servers, routing, middleware, making requests, templates, and database/sql โ€” the standard library web stack.

http.HandleFunc http.Server Middleware http.Client Templates database/sql ServeMux http.Get
๐Ÿš€

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 โ€” 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

โ„น๏ธ
Go 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

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

โ„น๏ธ
Use 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

โ„น๏ธ
database/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

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

Concept Call Notes
Register handlerhttp.HandleFunc(pattern, fn)Uses DefaultServeMux
Custom muxmux := http.NewServeMux()Preferred over DefaultServeMux
Method+path pattern"GET /users/{id}"Go 1.22+; use r.PathValue("id")
Start serverhttp.ListenAndServe(addr, mux)Blocks; returns only on error
With timeoutshttp.Server{ReadTimeout: โ€ฆ}Always set in production
Query paramsr.URL.Query().Get("key")Returns "" if absent
Request headerr.Header.Get("Authorization")Canonical header name
Set response headerw.Header().Set("Content-Type", โ€ฆ)Must be before WriteHeader
Write status codew.WriteHeader(http.StatusCreated)Sends headers; can only call once
JSON responsejson.NewEncoder(w).Encode(v)Set Content-Type first
Error responsehttp.Error(w, msg, code)Sets Content-Type text/plain
Redirecthttp.Redirect(w, r, url, 302)Use http.StatusSeeOther for POSTโ†’GET
Middlewarefunc(http.Handler) http.HandlerWrap and call next.ServeHTTP
Context valuer.WithContext(ctx)Pass values through middleware chain
Simple GEThttp.Get(url)Uses DefaultClient โ€” no timeout!
Custom clienthttp.Client{Timeout: 10s}Always set Timeout
Build requesthttp.NewRequestWithContext(ctx, โ€ฆ)Use for custom headers/context
Drain bodyio.Copy(io.Discard, resp.Body)Enables connection reuse
HTML templatetemplate.Must(template.ParseGlob(โ€ฆ))Parse at startup, not per request
Render templatetmpl.ExecuteTemplate(w, name, data)auto-escapes for XSS prevention
DB connectionsql.Open(driver, dsn)Lazy โ€” call Ping() to verify
Query rowsdb.QueryContext(ctx, sql, argsโ€ฆ)defer rows.Close(); check rows.Err()
Single rowdb.QueryRowContext(ctx, sql, โ€ฆ).Scan()Check sql.ErrNoRows
Execute SQLdb.ExecContext(ctx, sql, argsโ€ฆ)INSERT/UPDATE/DELETE
Transactiondb.BeginTx(ctx, nil)defer tx.Rollback(); tx.Commit()
Graceful shutdownsrv.Shutdown(ctx)Drains in-flight; use with signal.NotifyContext