Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
85: Building a Rate Limiter Middleware
Imagine we've got a public API that serves expensive data—let's say it's a high-resolution weather forecast that hits a paid third-party provider. If a single aggressive client decides to script a loop and hit our /forecast endpoint 500 times a second, we're going to get a massive bill, or worse, the provider will ban our API key. We need a way to tell the client, "Slow down," before the request even hits our business logic.
The "Quick and Dirty" Map
My first instinct when I tackle this is usually the simplest one: just keep a count of requests per IP address. I'll throw together a map where the key is the IP and the value is an integer. If the integer hits a limit, I'll return a 429 Too Many Requests.
type limiter struct {
counts map[string]int
}
func (l *limiter) Limit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
l.counts[ip]++
if l.counts[ip] > 10 {
http.Error(w, "Too many requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
I run this, and for the first few requests, it feels great. But then I remember that Go maps are not thread-safe. As soon as I hit this with a load testing tool like hey or ab, the server crashes instantly with a fatal error: concurrent map writes. This is a classic "gotcha" in Go middleware. Since every request runs in its own goroutine, we're hammering that map from dozens of directions at once.
The Panic and the Mutex
Alright, so I need to protect that map. I'll wrap it in a sync.Mutex. While I'm at it, I realize the current logic is broken: once a user hits 10 requests, they are blocked forever because I'm never resetting the counter. I'll add a crude "reset" by just wiping the map every minute using a goroutine.
type limiter struct {
mu sync.Mutex
counts map[string]int
}
func NewLimiter() *limiter {
l := &limiter{counts: make(map[string]int)}
go func() {
for range time.Tick(time.Minute) {
l.mu.Lock()
l.counts = make(map[string]int)
l.mu.Unlock()
}
}()
return l
}
This stops the crashing, but it's still a bit clunky. We've created a "fixed window" limiter. If a user sends 10 requests at 10:00:59 and another 10 at 10:01:01, they've essentially sent 20 requests in two seconds, which defeats the purpose of a strict rate limit. Plus, locking the entire map for every single request creates a bottleneck. In a high-traffic app, my middleware becomes the slowest part of the system.
Leveraging the Token Bucket
Instead of fighting with mutexes and timers, I'm going to use the golang.org/x/time/rate package. This implements a "Token Bucket" algorithm. Think of it like a bucket that fills with tokens at a steady rate. Every request takes a token. If the bucket is empty, the request is rejected. This allows for "bursts" (if the bucket is full, you can send a few requests quickly) while maintaining a strict average rate over time.
The tricky part here is that we need a separate bucket for every single IP address, otherwise, one user could exhaust the tokens for everyone on the internet.
import (
"net/http"
"sync"
"golang.org/x/time/rate"
)
type visitor struct {
limiter *rate.Limiter
lastSeen time.Time
}
var visitors = make(map[string]*visitor)
var mu sync.Mutex
func limitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr // In production, check X-Forwarded-For
mu.Lock()
v, exists := visitors[ip]
if !exists {
// Allow 5 requests per second, with a burst capacity of 10
v = &visitor{limiter: rate.NewLimiter(5, 10)}
visitors[ip] = v
}
v.lastSeen = time.Now()
mu.Unlock()
if !v.limiter.Allow() {
http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
The Final Leak
Wait, I've just introduced a memory leak. I'm adding a visitor struct to the visitors map for every single unique IP that hits my server. If I get a million unique visitors, that map grows indefinitely. I need a way to clean up old visitors who haven't been seen in a while.
I'll add a background cleanup loop that iterates through the map and deletes entries where lastSeen is older than, say, 3 minutes. This is the "professional" way to handle stateful middleware: create the limit, handle the concurrency, and always, always plan for the cleanup of the state you've created.
📋 Practical Task
Implement a Memory-Cleaning Loop for the Rate Limiter
In the lesson, I mentioned that the visitors map will grow indefinitely, leading to a memory leak. Your task is to complete the implementation by adding a cleanup function.
- Create a function
cleanupVisitors()that runs in its own goroutine. - The function should wake up every 1 minute.
- It must lock the
mumutex, iterate through thevisitorsmap, and delete any visitor who hasn't made a request in the last 5 minutes. - Integrate this function into the
main()function or the middleware initialization so it starts when the server begins.
There are no comments for now.