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
| Method | When |
|---|---|
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