限流不是大公司才需要的功能。一个公开表单、一个登录接口、一个 AI 调用入口,如果没有任何限制,很容易被误用、刷爆或拖垮下游依赖。对 Go 初学者来说,限流最值得先理解的是“保护边界”:限制请求进入系统的速度,而不是等数据库或外部服务扛不住后再处理错误。
本文用令牌桶思路写一个 HTTP 中间件。示例不依赖第三方库,便于理解原理。真实项目可以使用成熟库或网关限流,但理解基本模型后,你会更知道配置该怎么设。
令牌桶的直觉
令牌桶可以想象成一个小桶:系统按固定速度往桶里放令牌,桶满了就不再增加。每个请求进来要拿一个令牌,拿到就通过,拿不到就返回 429。桶容量决定允许的突发量,补充速度决定长期平均速率。
比如每秒补 5 个令牌,桶容量 10。平时没请求时桶会积到 10 个,突然来 10 个请求可以马上通过;之后如果继续高频请求,就只能按每秒 5 个的速度通过。
一个简单 Limiter
先写结构:
type TokenBucket struct {
mu sync.Mutex
tokens float64
capacity float64
rate float64
last time.Time
}
func NewTokenBucket(rate float64, capacity int) *TokenBucket {
return &TokenBucket{
tokens: float64(capacity),
capacity: float64(capacity),
rate: rate,
last: time.Now(),
}
}
允许请求:
func (b *TokenBucket) Allow() bool {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
elapsed := now.Sub(b.last).Seconds()
b.last = now
b.tokens += elapsed * b.rate
if b.tokens > b.capacity {
b.tokens = b.capacity
}
if b.tokens < 1 {
return false
}
b.tokens--
return true
}
这里用 float64 是为了表达“半个令牌”这类时间累计。初学者不用纠结细节,先理解它每次请求都会按经过时间补充令牌,然后尝试消耗一个。
做成 HTTP 中间件
全局限流:
func RateLimit(bucket *TokenBucket, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !bucket.Allow() {
w.Header().Set("Retry-After", "1")
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
使用:
bucket := NewTokenBucket(5, 10)
mux.Handle("/api/search", RateLimit(bucket, searchHandler))
这会限制整个接口总流量。对于内部小服务,全局限流已经有价值。但如果多个用户共享一个桶,一个用户刷接口会影响其他用户。公开接口更常见的是按 IP 或按用户限流。
按 IP 限流
维护一个 map:
type IPLimiter struct {
mu sync.Mutex
rate float64
burst int
buckets map[string]*TokenBucket
}
func NewIPLimiter(rate float64, burst int) *IPLimiter {
return &IPLimiter{
rate: rate,
burst: burst,
buckets: make(map[string]*TokenBucket),
}
}
获取 bucket:
func (l *IPLimiter) bucket(ip string) *TokenBucket {
l.mu.Lock()
defer l.mu.Unlock()
b, ok := l.buckets[ip]
if !ok {
b = NewTokenBucket(l.rate, l.burst)
l.buckets[ip] = b
}
return b
}
中间件:
func (l *IPLimiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
http.Error(w, "bad remote address", http.StatusBadRequest)
return
}
if !l.bucket(ip).Allow() {
http.Error(w, "too many requests", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
如果服务在反向代理后面,RemoteAddr 可能是代理地址。这时要由可信代理设置 X-Forwarded-For 或 X-Real-IP,应用只在确认来源可信时读取这些头。不要无条件相信客户端传来的 IP 头。
清理长期不用的 bucket
按 IP 限流会让 map 增长。可以记录最后访问时间:
type visitor struct {
bucket *TokenBucket
lastSeen time.Time
}
定期清理:
func (l *IPLimiter) Cleanup(maxIdle time.Duration) {
l.mu.Lock()
defer l.mu.Unlock()
now := time.Now()
for ip, v := range l.visitors {
if now.Sub(v.lastSeen) > maxIdle {
delete(l.visitors, ip)
}
}
}
示例为了简短前面用了 map[string]*TokenBucket,实际项目可以改成 map[string]visitor。限流状态本身也需要管理,否则防护代码会变成内存增长来源。
测试限流
测试全局桶比较容易:
func TestTokenBucketAllowsBurst(t *testing.T) {
b := NewTokenBucket(1, 2)
if !b.Allow() || !b.Allow() {
t.Fatal("expected first two requests to pass")
}
if b.Allow() {
t.Fatal("expected third request to be limited")
}
}
涉及时间的测试容易不稳定。更严谨的设计是把时钟注入 limiter,但入门阶段先测不依赖等待的 burst 行为。不要在测试里随便 time.Sleep(2 * time.Second),测试会变慢,还可能在 CI 上抖动。
小结
限流的基本目标是保护系统边界。令牌桶通过“固定速度补充令牌、请求消耗令牌”来同时支持平均速率和短暂突发。Go 里可以把它封装成 HTTP 中间件,再根据需要做全局限流、按 IP 限流或按用户限流。
实际落地时要注意代理 IP、状态清理、429 响应和测试稳定性。限流不是越严格越好,而是要匹配业务容量和用户体验。先保护关键接口,再逐步观察和调整。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。