Back to Notes

Go HTTP Server

Go HTTP Server

net/http ships a production-ready HTTP server in the standard library. No framework needed for most services.


Minimal Server

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, World!")
    })

    http.ListenAndServe(":8080", nil) // uses DefaultServeMux
}

Handler Interface

http.Handler is the core interface:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

http.HandlerFunc converts a function to a Handler:

type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }

Custom ServeMux (preferred over DefaultServeMux)

mux := http.NewServeMux()
mux.HandleFunc("GET /users", listUsers)          // Go 1.22+ method+path routing
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /users/{id}", getUser)       // path parameter (Go 1.22+)

srv := &http.Server{
    Addr:         ":8080",
    Handler:      mux,
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
    IdleTimeout:  120 * time.Second,
}
srv.ListenAndServe()

Always set timeouts — without them, slow clients can exhaust goroutines.


Reading Path Parameters (Go 1.22+)

mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id") // "123"
    // ...
})

Pre-1.22: extract from r.URL.Path manually or use gorilla/mux.


Writing JSON Responses

func writeJSON(w http.ResponseWriter, status int, v any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(v)
}

// Usage
type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func getUser(w http.ResponseWriter, r *http.Request) {
    user := User{ID: 1, Name: "Alice"}
    writeJSON(w, http.StatusOK, user)
}

Reading JSON Request Body

func createUser(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }

    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        http.Error(w, "invalid JSON", http.StatusBadRequest)
        return
    }
    defer r.Body.Close()

    // validate + save...
    writeJSON(w, http.StatusCreated, map[string]string{"id": "42"})
}

Middleware Pattern

Middleware wraps http.Handler — accepts and returns http.Handler:

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()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, 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 token == "" {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Chaining middleware (apply outermost last)
func chain(h http.Handler, m ...Middleware) http.Handler {
    for i := len(m) - 1; i >= 0; i-- {
        h = m[i](h)
    }
    return h
}

// Usage
mux.Handle("/api/users", chain(http.HandlerFunc(listUsers), logging, auth))

Context in Handlers

func getUser(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
    defer cancel()

    user, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = ?", r.PathValue("id"))
    if err != nil {
        if errors.Is(ctx.Err(), context.DeadlineExceeded) {
            http.Error(w, "timeout", http.StatusGatewayTimeout)
            return
        }
        http.Error(w, "internal error", http.StatusInternalServerError)
        return
    }
    writeJSON(w, http.StatusOK, user)
}

Graceful Shutdown

srv := &http.Server{Addr: ":8080", Handler: mux}

go func() {
    if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}()

// Wait for interrupt
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx) // finish in-flight requests, then close

ResponseWriter Gotchas

// WriteHeader must be called BEFORE Write
w.WriteHeader(http.StatusCreated)
w.Write(body)

// If you call Write without WriteHeader, 200 is sent automatically
// CANNOT change status after Write — headers are sent immediately
MethodWhen
w.Header().Set(k, v)Before WriteHeader or Write
w.WriteHeader(status)Once, before Write
w.Write(body)Any time, triggers 200 if WriteHeader not called

Common Patterns

// Health check endpoint
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
})

// Query parameters
func search(w http.ResponseWriter, r *http.Request) {
    q := r.URL.Query().Get("q")
    page := r.URL.Query().Get("page") // default "" if missing
}

// CORS middleware
func cors(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Interview Talking Points

  • "Go's net/http is production-ready without a framework. For routing, Go 1.22 added method + path syntax natively."
  • "Middleware is just a function that takes and returns http.Handler — composition over inheritance."
  • "Always set Read/WriteTimeout on http.Server — without them, slow clients block goroutines forever."
  • "Graceful shutdown: call srv.Shutdown(ctx) which stops accepting new connections but waits for in-flight requests to complete."

Related

  • [[Go/Context]] — r.Context(), WithTimeout in handlers
  • [[Go/HTTP Clients]] — client-side HTTP
  • [[Go/Testing]] — testing HTTP handlers with httptest
  • [[Go/Errors]] — error handling patterns
  • [[API Gateway]] — API gateway patterns