Skip to content

🌐 HTTP Services

"HTTP is the lingua franca of the modern web."
Go's `net/http` là một trong những best-designed HTTP libraries trong bất kỳ ngôn ngữ nào.

🏗️ HTTP Fundamentals in Go

Basic Server

go
package main

import (
    "encoding/json"
    "net/http"
)

func main() {
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    })
    
    http.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
        users := []string{"Alice", "Bob"}
        json.NewEncoder(w).Encode(users)
    })
    
    http.ListenAndServe(":8080", nil)
}

Handler Interface

go
// The fundamental building block
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

// HandlerFunc adapter - any func can be a Handler
type HandlerFunc func(ResponseWriter, *Request)

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

⚔️ Tradeoff: Router Selection

RouterPerformanceFeaturesWhen to Use
net/http★★★★★BasicSmall services, embedded
chi★★★★★ModerateMost production services
gin★★★★☆RichHigh-traffic APIs
echo★★★★☆RichREST APIs
gorilla/mux★★★☆☆ModerateLegacy (deprecated)

Recommendation: chi (Idiomatic, Fast, Composable)

go
import "github.com/go-chi/chi/v5"

func main() {
    r := chi.NewRouter()
    
    // Middleware
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    
    // Routes
    r.Get("/", homeHandler)
    
    r.Route("/api/v1", func(r chi.Router) {
        r.Route("/users", func(r chi.Router) {
            r.Get("/", listUsers)
            r.Post("/", createUser)
            r.Get("/{id}", getUser)
            r.Put("/{id}", updateUser)
            r.Delete("/{id}", deleteUser)
        })
    })
    
    http.ListenAndServe(":8080", r)
}

🔧 Middleware Patterns

Middleware Chain

go
// Middleware signature
type Middleware func(http.Handler) http.Handler

// Logging middleware
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Wrap ResponseWriter to capture status code
        ww := &responseWriter{ResponseWriter: w, status: 200}
        
        next.ServeHTTP(ww, r)
        
        log.Printf("%s %s %d %v",
            r.Method, r.URL.Path, ww.status, time.Since(start))
    })
}

type responseWriter struct {
    http.ResponseWriter
    status int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.status = code
    rw.ResponseWriter.WriteHeader(code)
}

// Recovery middleware
func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic: %v\n%s", err, debug.Stack())
                http.Error(w, "Internal Server Error", 500)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// Auth middleware
func AuthMiddleware(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", 401)
            return
        }
        
        user, err := validateToken(token)
        if err != nil {
            http.Error(w, "Invalid token", 401)
            return
        }
        
        // Add user to context
        ctx := context.WithValue(r.Context(), userContextKey, user)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Context Propagation

go
type contextKey string

const (
    requestIDKey contextKey = "request_id"
    userKey      contextKey = "user"
)

// Set in middleware
func RequestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        reqID := r.Header.Get("X-Request-ID")
        if reqID == "" {
            reqID = uuid.New().String()
        }
        
        ctx := context.WithValue(r.Context(), requestIDKey, reqID)
        w.Header().Set("X-Request-ID", reqID)
        
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Get in handler
func GetRequestID(ctx context.Context) string {
    if v := ctx.Value(requestIDKey); v != nil {
        return v.(string)
    }
    return ""
}

⏱️ Timeouts & Graceful Shutdown

Server Timeouts (Critical!)

go
srv := &http.Server{
    Addr:         ":8080",
    Handler:      router,
    ReadTimeout:  5 * time.Second,   // Read request body
    WriteTimeout: 10 * time.Second,  // Write response
    IdleTimeout:  120 * time.Second, // Keep-alive connections
}

// For specific handlers with long operations
func longOperation(w http.ResponseWriter, r *http.Request) {
    // Create context with timeout
    ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
    defer cancel()
    
    result, err := expensiveOperation(ctx)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "timeout", http.StatusGatewayTimeout)
            return
        }
        http.Error(w, err.Error(), 500)
        return
    }
    
    json.NewEncoder(w).Encode(result)
}

Graceful Shutdown

go
func main() {
    srv := &http.Server{
        Addr:    ":8080",
        Handler: router,
    }
    
    // Start server in goroutine
    go func() {
        log.Println("Server starting on :8080")
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("ListenAndServe: %v", err)
        }
    }()
    
    // Wait for interrupt signal
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    
    log.Println("Shutting down server...")
    
    // Give outstanding requests 30 seconds to complete
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }
    
    log.Println("Server exited properly")
}

📝 Request Validation

JSON Request Handling

go
type CreateUserRequest struct {
    Name  string `json:"name" validate:"required,min=2,max=100"`
    Email string `json:"email" validate:"required,email"`
    Age   int    `json:"age" validate:"gte=0,lte=150"`
}

func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    
    // Decode JSON
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        h.respondError(w, http.StatusBadRequest, "invalid JSON")
        return
    }
    
    // Validate
    if err := h.validator.Struct(req); err != nil {
        h.respondError(w, http.StatusBadRequest, formatValidationError(err))
        return
    }
    
    // Process...
    user, err := h.service.Create(r.Context(), &req)
    if err != nil {
        h.handleError(w, err)
        return
    }
    
    h.respondJSON(w, http.StatusCreated, user)
}

🚨 Error Responses (RFC 7807)

Problem Details Standard

go
// Problem Details response (RFC 7807)
type ProblemDetails struct {
    Type     string `json:"type"`
    Title    string `json:"title"`
    Status   int    `json:"status"`
    Detail   string `json:"detail,omitempty"`
    Instance string `json:"instance,omitempty"`
}

func respondError(w http.ResponseWriter, status int, detail string) {
    problem := ProblemDetails{
        Type:   fmt.Sprintf("https://api.example.com/problems/%d", status),
        Title:  http.StatusText(status),
        Status: status,
        Detail: detail,
    }
    
    w.Header().Set("Content-Type", "application/problem+json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(problem)
}

// Example response:
// {
//   "type": "https://api.example.com/problems/400",
//   "title": "Bad Request",
//   "status": 400,
//   "detail": "email: must be a valid email address"
// }
)}}

Error Mapping

go
func (h *Handler) handleError(w http.ResponseWriter, err error) {
    switch {
    case errors.Is(err, ErrNotFound):
        respondError(w, http.StatusNotFound, err.Error())
    case errors.Is(err, ErrConflict):
        respondError(w, http.StatusConflict, err.Error())
    case errors.Is(err, ErrValidation):
        respondError(w, http.StatusBadRequest, err.Error())
    case errors.Is(err, context.DeadlineExceeded):
        respondError(w, http.StatusGatewayTimeout, "request timeout")
    default:
        log.Printf("internal error: %v", err)
        respondError(w, http.StatusInternalServerError, "internal error")
    }
}

💻 Engineering Example: Production HTTP API

go
package main

import (
    "context"
    "encoding/json"
    "errors"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
    "github.com/go-playground/validator/v10"
)

// Handler with dependencies
type UserHandler struct {
    service   UserService
    validator *validator.Validate
    logger    *log.Logger
}

func NewUserHandler(svc UserService) *UserHandler {
    return &UserHandler{
        service:   svc,
        validator: validator.New(),
        logger:    log.Default(),
    }
}

// Response helpers
func (h *UserHandler) respondJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func (h *UserHandler) respondError(w http.ResponseWriter, status int, message string) {
    h.respondJSON(w, status, map[string]string{"error": message})
}

// Handlers
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
    users, err := h.service.List(r.Context())
    if err != nil {
        h.logger.Printf("list users error: %v", err)
        h.respondError(w, http.StatusInternalServerError, "failed to list users")
        return
    }
    h.respondJSON(w, http.StatusOK, users)
}

func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    
    user, err := h.service.GetByID(r.Context(), id)
    if err != nil {
        if errors.Is(err, ErrNotFound) {
            h.respondError(w, http.StatusNotFound, "user not found")
            return
        }
        h.logger.Printf("get user error: %v", err)
        h.respondError(w, http.StatusInternalServerError, "failed to get user")
        return
    }
    
    h.respondJSON(w, http.StatusOK, user)
}

func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        h.respondError(w, http.StatusBadRequest, "invalid JSON")
        return
    }
    
    if err := h.validator.Struct(req); err != nil {
        h.respondError(w, http.StatusBadRequest, err.Error())
        return
    }
    
    user, err := h.service.Create(r.Context(), &req)
    if err != nil {
        h.logger.Printf("create user error: %v", err)
        h.respondError(w, http.StatusInternalServerError, "failed to create user")
        return
    }
    
    h.respondJSON(w, http.StatusCreated, user)
}

// Router setup
func NewRouter(userHandler *UserHandler) chi.Router {
    r := chi.NewRouter()
    
    // Global middleware
    r.Use(middleware.RequestID)
    r.Use(middleware.RealIP)
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    r.Use(middleware.Timeout(30 * time.Second))
    
    // CORS (if needed)
    r.Use(func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            w.Header().Set("Access-Control-Allow-Origin", "*")
            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
            w.Header().Set("Access-Control-Allow-Headers", "Accept, Content-Type, Authorization")
            
            if r.Method == "OPTIONS" {
                w.WriteHeader(http.StatusOK)
                return
            }
            
            next.ServeHTTP(w, r)
        })
    })
    
    // Health check
    r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("OK"))
    })
    
    // API routes
    r.Route("/api/v1", func(r chi.Router) {
        r.Route("/users", func(r chi.Router) {
            r.Get("/", userHandler.List)
            r.Post("/", userHandler.Create)
            r.Get("/{id}", userHandler.Get)
        })
    })
    
    return r
}

// Main with graceful shutdown
func main() {
    // Initialize dependencies...
    userHandler := NewUserHandler(userService)
    router := NewRouter(userHandler)
    
    srv := &http.Server{
        Addr:         ":8080",
        Handler:      router,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }
    
    // Graceful shutdown
    go func() {
        log.Println("Server starting on :8080")
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("ListenAndServe: %v", err)
        }
    }()
    
    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)
    log.Println("Server exited")
}

Ship-to-Prod Checklist

Server Configuration

  • [ ] Timeouts set: ReadTimeout, WriteTimeout, IdleTimeout
  • [ ] Graceful shutdown implemented
  • [ ] Health endpoint at /health or /healthz
  • [ ] Ready endpoint at /ready (if using K8s)

Middleware Stack

  • [ ] Logging middleware
  • [ ] Recovery middleware (panic handling)
  • [ ] Request ID propagation
  • [ ] CORS configured (if needed)
  • [ ] Auth middleware (if needed)

Request/Response

  • [ ] Content-Type headers set correctly
  • [ ] Proper status codes returned
  • [ ] Error responses follow consistent format
  • [ ] Request validation implemented

Security

  • [ ] No sensitive data in logs
  • [ ] Rate limiting considered
  • [ ] Input sanitization
  • [ ] TLS in production (or behind proxy)

📊 Summary

ComponentRecommendation
Routerchi for most cases
TimeoutsAlways configure
ShutdownGraceful with 30s deadline
ErrorsRFC 7807 Problem Details
MiddlewareChain: Logger → Recovery → Auth

➡️ Tiếp theo

HTTP Services nắm vững rồi! Tiếp theo: Database Patterns - Production database access patterns.