Skip to content

Go Web Services Best Practices

Objective: Master senior-level Go web service patterns for production systems. When you need to build robust, scalable HTTP services, when you want to follow proven methodologies, when you need enterprise-grade web service patternsโ€”these best practices become your weapon of choice.

Core Principles

  • HTTP-First Design: Leverage HTTP semantics and status codes
  • Middleware Architecture: Use middleware for cross-cutting concerns
  • Graceful Shutdown: Handle shutdown signals properly
  • Request/Response Validation: Validate all inputs and outputs
  • Security by Default: Implement security measures from the start

HTTP Server Setup

Basic Server Structure

// internal/server/server.go
package server

import (
    "context"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

// Server represents an HTTP server
type Server struct {
    httpServer *http.Server
    router     *http.ServeMux
    config     *Config
}

// Config holds server configuration
type Config struct {
    Port         string
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
    IdleTimeout  time.Duration
}

// NewServer creates a new HTTP server
func NewServer(config *Config) *Server {
    router := http.NewServeMux()

    server := &Server{
        router: router,
        config: config,
    }

    server.setupRoutes()

    return server
}

// setupRoutes configures the HTTP routes
func (s *Server) setupRoutes() {
    // Health check
    s.router.HandleFunc("/health", s.healthHandler)

    // API routes
    s.router.HandleFunc("/api/users", s.usersHandler)
    s.router.HandleFunc("/api/users/", s.userHandler)

    // Static files
    s.router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
}

// Start starts the HTTP server
func (s *Server) Start() error {
    s.httpServer = &http.Server{
        Addr:         s.config.Port,
        Handler:      s.router,
        ReadTimeout:  s.config.ReadTimeout,
        WriteTimeout: s.config.WriteTimeout,
        IdleTimeout:  s.config.IdleTimeout,
    }

    log.Printf("Starting server on %s", s.config.Port)
    return s.httpServer.ListenAndServe()
}

// StartTLS starts the HTTPS server
func (s *Server) StartTLS(certFile, keyFile string) error {
    s.httpServer = &http.Server{
        Addr:         s.config.Port,
        Handler:      s.router,
        ReadTimeout:  s.config.ReadTimeout,
        WriteTimeout: s.config.WriteTimeout,
        IdleTimeout:  s.config.IdleTimeout,
    }

    log.Printf("Starting TLS server on %s", s.config.Port)
    return s.httpServer.ListenAndServeTLS(certFile, keyFile)
}

// Stop gracefully stops the server
func (s *Server) Stop(ctx context.Context) error {
    log.Println("Shutting down server...")
    return s.httpServer.Shutdown(ctx)
}

// healthHandler handles health check requests
func (s *Server) healthHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, `{"status":"healthy","timestamp":"%s"}`, time.Now().UTC().Format(time.RFC3339))
}

Graceful Shutdown

// internal/server/graceful.go
package server

import (
    "context"
    "log"
    "os"
    "os/signal"
    "syscall"
    "time"
)

// RunWithGracefulShutdown runs the server with graceful shutdown
func (s *Server) RunWithGracefulShutdown() {
    // Start server in a goroutine
    go func() {
        if err := s.Start(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server failed to start: %v", err)
        }
    }()

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

    // Graceful shutdown with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := s.Stop(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }

    log.Println("Server exited")
}

Middleware Architecture

Middleware Interface

// internal/middleware/middleware.go
package middleware

import (
    "context"
    "net/http"
    "time"
)

// Middleware represents a middleware function
type Middleware func(http.Handler) http.Handler

// Chain chains multiple middlewares
func Chain(middlewares ...Middleware) Middleware {
    return func(next http.Handler) http.Handler {
        for i := len(middlewares) - 1; i >= 0; i-- {
            next = middlewares[i](next)
        }
        return next
    }
}

Logging Middleware

// internal/middleware/logging.go
package middleware

import (
    "log"
    "net/http"
    "time"
)

// LoggingMiddleware logs HTTP requests
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // Wrap the ResponseWriter to capture status code
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}

        next.ServeHTTP(wrapped, r)

        duration := time.Since(start)
        log.Printf("%s %s %d %v", r.Method, r.URL.Path, wrapped.statusCode, duration)
    })
}

// responseWriter wraps http.ResponseWriter to capture status code
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

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

CORS Middleware

// internal/middleware/cors.go
package middleware

import (
    "net/http"
)

// CORSMiddleware handles CORS headers
func CORSMiddleware(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", "Content-Type, Authorization")

        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusOK)
            return
        }

        next.ServeHTTP(w, r)
    })
}

Authentication Middleware

// internal/middleware/auth.go
package middleware

import (
    "context"
    "net/http"
    "strings"
)

// AuthMiddleware handles authentication
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            http.Error(w, "Authorization header required", http.StatusUnauthorized)
            return
        }

        token := strings.TrimPrefix(authHeader, "Bearer ")
        if token == authHeader {
            http.Error(w, "Invalid authorization format", http.StatusUnauthorized)
            return
        }

        // Validate token (implement your validation logic)
        userID, err := validateToken(token)
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }

        // Add user ID to context
        ctx := context.WithValue(r.Context(), "userID", userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// validateToken validates a JWT token
func validateToken(token string) (string, error) {
    // Implement JWT validation logic
    // Return user ID and error
    return "user123", nil
}

Rate Limiting Middleware

// internal/middleware/ratelimit.go
package middleware

import (
    "net/http"
    "sync"
    "time"
)

// RateLimiter implements token bucket rate limiting
type RateLimiter struct {
    mu       sync.Mutex
    tokens   int
    capacity int
    rate     time.Duration
    lastTime time.Time
}

// NewRateLimiter creates a new rate limiter
func NewRateLimiter(capacity int, rate time.Duration) *RateLimiter {
    return &RateLimiter{
        tokens:   capacity,
        capacity: capacity,
        rate:     rate,
        lastTime: time.Now(),
    }
}

// Allow checks if a request is allowed
func (rl *RateLimiter) Allow() bool {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    now := time.Now()
    elapsed := now.Sub(rl.lastTime)

    // Add tokens based on elapsed time
    tokensToAdd := int(elapsed / rl.rate)
    if tokensToAdd > 0 {
        rl.tokens = min(rl.capacity, rl.tokens+tokensToAdd)
        rl.lastTime = now
    }

    if rl.tokens > 0 {
        rl.tokens--
        return true
    }

    return false
}

// RateLimitMiddleware implements rate limiting
func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

// min returns the minimum of two integers
func min(a, b int) int {
    if a < b {
        return a
    }
    return b
}

Request/Response Handling

JSON Request/Response

// internal/handler/json.go
package handler

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

// JSONResponse represents a JSON response
type JSONResponse struct {
    Data    interface{} `json:"data,omitempty"`
    Error   string      `json:"error,omitempty"`
    Message string      `json:"message,omitempty"`
}

// WriteJSON writes a JSON response
func WriteJSON(w http.ResponseWriter, statusCode int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(statusCode)

    response := JSONResponse{Data: data}
    json.NewEncoder(w).Encode(response)
}

// WriteError writes an error response
func WriteError(w http.ResponseWriter, statusCode int, message string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(statusCode)

    response := JSONResponse{Error: message}
    json.NewEncoder(w).Encode(response)
}

// ReadJSON reads JSON from request body
func ReadJSON(r *http.Request, v interface{}) error {
    if r.Header.Get("Content-Type") != "application/json" {
        return fmt.Errorf("content-type must be application/json")
    }

    return json.NewDecoder(r.Body).Decode(v)
}

Request Validation

// internal/handler/validation.go
package handler

import (
    "fmt"
    "net/http"
    "strings"
)

// Validator represents a request validator
type Validator interface {
    Validate() error
}

// UserRequest represents a user creation request
type UserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Validate validates the user request
func (ur *UserRequest) Validate() error {
    if strings.TrimSpace(ur.Name) == "" {
        return fmt.Errorf("name is required")
    }

    if strings.TrimSpace(ur.Email) == "" {
        return fmt.Errorf("email is required")
    }

    if !isValidEmail(ur.Email) {
        return fmt.Errorf("invalid email format")
    }

    return nil
}

// isValidEmail validates email format
func isValidEmail(email string) bool {
    return strings.Contains(email, "@") && strings.Contains(email, ".")
}

Handler Patterns

RESTful Handlers

// internal/handler/user.go
package handler

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strconv"
    "strings"
)

// UserHandler handles user-related HTTP requests
type UserHandler struct {
    userService UserService
}

// NewUserHandler creates a new user handler
func NewUserHandler(userService UserService) *UserHandler {
    return &UserHandler{
        userService: userService,
    }
}

// CreateUser handles user creation
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        WriteError(w, http.StatusMethodNotAllowed, "Method not allowed")
        return
    }

    var req UserRequest
    if err := ReadJSON(r, &req); err != nil {
        WriteError(w, http.StatusBadRequest, "Invalid JSON")
        return
    }

    if err := req.Validate(); err != nil {
        WriteError(w, http.StatusBadRequest, err.Error())
        return
    }

    user, err := h.userService.CreateUser(&req)
    if err != nil {
        WriteError(w, http.StatusInternalServerError, "Failed to create user")
        return
    }

    WriteJSON(w, http.StatusCreated, user)
}

// GetUser handles user retrieval
func (h *UserHandler) GetUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        WriteError(w, http.StatusMethodNotAllowed, "Method not allowed")
        return
    }

    userID := extractUserID(r.URL.Path)
    if userID == "" {
        WriteError(w, http.StatusBadRequest, "User ID required")
        return
    }

    user, err := h.userService.GetUser(userID)
    if err != nil {
        WriteError(w, http.StatusNotFound, "User not found")
        return
    }

    WriteJSON(w, http.StatusOK, user)
}

// UpdateUser handles user updates
func (h *UserHandler) UpdateUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPut {
        WriteError(w, http.StatusMethodNotAllowed, "Method not allowed")
        return
    }

    userID := extractUserID(r.URL.Path)
    if userID == "" {
        WriteError(w, http.StatusBadRequest, "User ID required")
        return
    }

    var req UserRequest
    if err := ReadJSON(r, &req); err != nil {
        WriteError(w, http.StatusBadRequest, "Invalid JSON")
        return
    }

    if err := req.Validate(); err != nil {
        WriteError(w, http.StatusBadRequest, err.Error())
        return
    }

    user, err := h.userService.UpdateUser(userID, &req)
    if err != nil {
        WriteError(w, http.StatusInternalServerError, "Failed to update user")
        return
    }

    WriteJSON(w, http.StatusOK, user)
}

// DeleteUser handles user deletion
func (h *UserHandler) DeleteUser(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodDelete {
        WriteError(w, http.StatusMethodNotAllowed, "Method not allowed")
        return
    }

    userID := extractUserID(r.URL.Path)
    if userID == "" {
        WriteError(w, http.StatusBadRequest, "User ID required")
        return
    }

    if err := h.userService.DeleteUser(userID); err != nil {
        WriteError(w, http.StatusInternalServerError, "Failed to delete user")
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

// extractUserID extracts user ID from URL path
func extractUserID(path string) string {
    parts := strings.Split(path, "/")
    if len(parts) >= 3 {
        return parts[3]
    }
    return ""
}

Error Handling

// internal/handler/error.go
package handler

import (
    "fmt"
    "net/http"
)

// ErrorHandler handles errors
type ErrorHandler struct {
    logger Logger
}

// NewErrorHandler creates a new error handler
func NewErrorHandler(logger Logger) *ErrorHandler {
    return &ErrorHandler{
        logger: logger,
    }
}

// HandleError handles application errors
func (eh *ErrorHandler) HandleError(w http.ResponseWriter, r *http.Request, err error) {
    eh.logger.Error("Request failed", map[string]interface{}{
        "method": r.Method,
        "path":   r.URL.Path,
        "error":  err.Error(),
    })

    switch err.(type) {
    case *ValidationError:
        WriteError(w, http.StatusBadRequest, err.Error())
    case *NotFoundError:
        WriteError(w, http.StatusNotFound, err.Error())
    case *UnauthorizedError:
        WriteError(w, http.StatusUnauthorized, err.Error())
    default:
        WriteError(w, http.StatusInternalServerError, "Internal server error")
    }
}

// Custom error types
type ValidationError struct {
    Message string
}

func (ve *ValidationError) Error() string {
    return ve.Message
}

type NotFoundError struct {
    Resource string
}

func (nfe *NotFoundError) Error() string {
    return fmt.Sprintf("%s not found", nfe.Resource)
}

type UnauthorizedError struct {
    Message string
}

func (ue *UnauthorizedError) Error() string {
    return ue.Message
}

Security Best Practices

HTTPS Configuration

// internal/server/tls.go
package server

import (
    "crypto/tls"
    "net/http"
)

// TLSConfig holds TLS configuration
type TLSConfig struct {
    CertFile string
    KeyFile  string
    MinTLS   uint16
}

// ConfigureTLS configures TLS settings
func (s *Server) ConfigureTLS(config *TLSConfig) {
    tlsConfig := &tls.Config{
        MinVersion: config.MinTLS,
        CipherSuites: []uint16{
            tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
            tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
        },
    }

    s.httpServer.TLSConfig = tlsConfig
}

Security Headers

// internal/middleware/security.go
package middleware

import "net/http"

// SecurityHeadersMiddleware adds security headers
func SecurityHeadersMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
        w.Header().Set("Content-Security-Policy", "default-src 'self'")

        next.ServeHTTP(w, r)
    })
}

Performance Optimization

Connection Pooling

// internal/server/pool.go
package server

import (
    "net"
    "net/http"
    "time"
)

// ConfigureConnectionPool configures HTTP connection pooling
func (s *Server) ConfigureConnectionPool() {
    transport := &http.Transport{
        MaxIdleConns:        100,
        MaxIdleConnsPerHost: 10,
        IdleConnTimeout:    90 * time.Second,
        DialContext: (&net.Dialer{
            Timeout:   30 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
    }

    s.httpServer.Transport = transport
}

Compression Middleware

// internal/middleware/compression.go
package middleware

import (
    "compress/gzip"
    "net/http"
    "strings"
)

// CompressionMiddleware adds gzip compression
func CompressionMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
            next.ServeHTTP(w, r)
            return
        }

        w.Header().Set("Content-Encoding", "gzip")
        gz := gzip.NewWriter(w)
        defer gz.Close()

        gzw := &gzipResponseWriter{ResponseWriter: w, Writer: gz}
        next.ServeHTTP(gzw, r)
    })
}

// gzipResponseWriter wraps http.ResponseWriter with gzip
type gzipResponseWriter struct {
    http.ResponseWriter
    *gzip.Writer
}

Testing Web Services

HTTP Testing

// internal/handler/user_test.go
package handler

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestUserHandler_CreateUser(t *testing.T) {
    // Arrange
    mockService := &MockUserService{}
    handler := NewUserHandler(mockService)

    userReq := UserRequest{
        Name:  "John Doe",
        Email: "john@example.com",
    }

    jsonData, err := json.Marshal(userReq)
    require.NoError(t, err)

    req := httptest.NewRequest("POST", "/api/users", bytes.NewBuffer(jsonData))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()

    // Act
    handler.CreateUser(w, req)

    // Assert
    assert.Equal(t, http.StatusCreated, w.Code)
    assert.Equal(t, "application/json", w.Header().Get("Content-Type"))

    var response JSONResponse
    err = json.Unmarshal(w.Body.Bytes(), &response)
    require.NoError(t, err)
    assert.NotNil(t, response.Data)
}

func TestUserHandler_CreateUser_InvalidJSON(t *testing.T) {
    // Arrange
    mockService := &MockUserService{}
    handler := NewUserHandler(mockService)

    req := httptest.NewRequest("POST", "/api/users", bytes.NewBufferString("invalid json"))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()

    // Act
    handler.CreateUser(w, req)

    // Assert
    assert.Equal(t, http.StatusBadRequest, w.Code)

    var response JSONResponse
    err := json.Unmarshal(w.Body.Bytes(), &response)
    require.NoError(t, err)
    assert.Equal(t, "Invalid JSON", response.Error)
}

TL;DR Runbook

Quick Start

// 1. Basic HTTP server
func main() {
    server := NewServer(&Config{
        Port:         ":8080",
        ReadTimeout:  30 * time.Second,
        WriteTimeout:   30 * time.Second,
        IdleTimeout:   120 * time.Second,
    })

    server.RunWithGracefulShutdown()
}

// 2. Middleware chain
middleware := Chain(
    LoggingMiddleware,
    CORSMiddleware,
    AuthMiddleware,
    SecurityHeadersMiddleware,
)

// 3. Handler with validation
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
    var req UserRequest
    if err := ReadJSON(r, &req); err != nil {
        WriteError(w, http.StatusBadRequest, "Invalid JSON")
        return
    }

    if err := req.Validate(); err != nil {
        WriteError(w, http.StatusBadRequest, err.Error())
        return
    }

    // Process request...
}

Essential Patterns

// Error handling
func (h *Handler) HandleRequest(w http.ResponseWriter, r *http.Request) {
    if err := h.processRequest(r); err != nil {
        h.errorHandler.HandleError(w, r, err)
        return
    }

    WriteJSON(w, http.StatusOK, result)
}

// Middleware usage
router.Handle("/api/users", middleware(handler.CreateUser))

// Graceful shutdown
server.RunWithGracefulShutdown()

This guide provides the complete machinery for building production-ready web services in Go applications. Each pattern includes implementation examples, security considerations, and real-world usage patterns for enterprise deployment.