Skip to content

Go Middleware — HTTP Middleware Patterns for Logging, Auth, Rate Limiting, and CORS

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Go Middleware. We cover key concepts, practical examples, and best practices to help you master this topic.

Go HTTP middleware chains functions that wrap http.Handler for cross-cutting concerns like logging, authentication, rate limiting, and CORS.

What You'll Learn

  • Middleware pattern and chaining
  • Common middleware implementations
  • Context-based middleware
  • Third-party middleware

Why It Matters

Middleware separates concerns. Docker uses middleware for auth. Kubernetes uses middleware for audit logging. DodaZIP uses middleware for request tracing.

Real-World Use

Authentication, request logging, rate limiting, CORS, metrics collection, request validation, tracing.

flowchart LR
    A["Middleware"] --> B["Logging"]
    A --> C["Auth"]
    A --> D["Rate Limit"]
    A --> E["CORS"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Middleware Pattern

func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Before handler
        next.ServeHTTP(w, r)
        // After handler
    })
}

Logging Middleware

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

        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)
    })
}

type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

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

Authentication Middleware

func AuthMiddleware(validTokens map[string]string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := r.Header.Get("Authorization")

            if token == "" || !strings.HasPrefix(token, "Bearer ") {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return
            }

            token = strings.TrimPrefix(token, "Bearer ")
            userID, ok := validTokens[token]
            if !ok {
                http.Error(w, "Invalid token", http.StatusUnauthorized)
                return
            }

            ctx := context.WithValue(r.Context(), "user_id", userID)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

Rate Limiting Middleware

func RateLimitMiddleware(requests int, per time.Duration) func(http.Handler) http.Handler {
    type client struct {
        count    int
        resetAt time.Time
    }
    var mu sync.Mutex
    clients := make(map[string]*client)

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ip := r.RemoteAddr

            mu.Lock()
            c, exists := clients[ip]
            if !exists || time.Now().After(c.resetAt) {
                c = &client{resetAt: time.Now().Add(per)}
                clients[ip] = c
            }
            c.count++
            mu.Unlock()

            if c.count > requests {
                http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
                return
            }

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

CORS Middleware

func CORSMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
    origins := make(map[string]bool)
    for _, o := range allowedOrigins { origins[o] = true }

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            origin := r.Header.Get("Origin")

            if origins[origin] || origins["*"] {
                w.Header().Set("Access-Control-Allow-Origin", origin)
            }

            w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
            w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
            w.Header().Set("Access-Control-Max-Age", "86400")

            if r.Method == "OPTIONS" {
                w.WriteHeader(http.StatusNoContent)
                return
            }

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

Middleware Chain

type Middleware func(http.Handler) http.Handler

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

func main() {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, World!"))
    })

    wrapped := Chain(handler,
        LoggingMiddleware,
        AuthMiddleware(validTokens),
        RateLimitMiddleware(100, time.Minute),
        CORSMiddleware([]string{"*"}),
    )

    http.ListenAndServe(":8080", wrapped)
}

Common Mistakes

1. Not Passing Context

// Use r.WithContext to pass values to handlers
ctx := context.WithValue(r.Context(), key, val)
next.ServeHTTP(w, r.WithContext(ctx))

2. Deadlock in Middleware

Rate limiting middleware using maps without mutex protection causes data races.

3. Swallowing Panics

Always use recovery middleware as the outermost layer. Never let panics escape.

4. Writing Response in Wrong Order

Set headers before writing body. Status code defaults to 200 if not set.

5. Blocking in Middleware

Don't block the Goroutine in middleware. Use async patterns for heavy work.

Practice Questions

1. What is the middleware pattern? A function that takes http.Handler and returns http.Handler. It wraps the handler with pre/post processing.

2. How do you pass data from middleware to handler? Use context.WithValue to store data in request context. Handler retrieves with r.Context().Value(key).

3. What is the purpose of CORS middleware? Controls which origins can access the API. Sets Access-Control-* headers.

4. How do you limit requests per IP? Track request counts in a map keyed by IP. Check count before passing to next handler.

Challenge: Build a middleware that adds request ID to every response header.

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

FAQ

{{< faq question="What is the order of middleware execution?" >}} Outermost middleware executes first (pre), then inner middleware, then handler. Response flows back through middleware in reverse order. {{< /faq >}}

{{< faq question="Can middleware abort the request?" >}} Yes. Call http.Error or w.WriteHeader to send response. Don't call next.ServeHTTP to abort. {{< /faq >}}

{{< faq question="How do I make middleware configurable?" >}} Return a closure from a Factory function: func MyMiddleware(config Config) func(http.Handler) http.Handler. {{< /faq >}}

{{< faq question="What is the difference between Gin and net/http middleware?" >}} Same pattern. Gin uses gin.HandlerFunc and c.Next(). net/http uses http.Handler chaining. {{< /faq >}}

{{< faq question="How do I measure request duration?" >}} Record time.Now() before next.ServeHTTP, calculate duration after. log.Printf or record metric. {{< /faq >}}

Try It Yourself

package main

import (
    "fmt"
    "net/http"
)

func hello(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello, World!")
}

func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Printf("Request: %s %s\n", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

func main() {
    http.Handle("/", logging(http.HandlerFunc(hello)))
    http.ListenAndServe(":8080", nil)
}

Expected output — logs each request and responds with "Hello, World!".

What's Next

Now that you understand middleware, explore testing HTTP handlers.

Topic Description Link
Go Testing HTTP Testing HTTP handlers {{< ref "42-testing-http" >}}
Go Web Frameworks Web frameworks {{< ref "40-web-frameworks" >}}
Go Deployment Deploying Go apps {{< ref "43-deployment" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro

Home Browse Go