main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
package main

import (
	"context"
	"fmt"
	"log"
	"sync"
	"time"
)

// TokenBucket implements a thread-safe token bucket rate limiter
type TokenBucket struct {
	mu           sync.Mutex
	capacity     int           // Maximum number of tokens
	tokens       int           // Current number of tokens
	refillRate   int           // Tokens to add per refill interval
	refillPeriod time.Duration // Interval for refilling tokens
	lastRefill   time.Time     // Timestamp of last refill
}

// NewTokenBucket creates a new TokenBucket with specified capacity and refill rate
func NewTokenBucket(capacity, refillRate int, refillPeriod time.Duration) *TokenBucket {
	return &TokenBucket{
		capacity:     capacity,
		tokens:       capacity, // Start with full bucket
		refillRate:   refillRate,
		refillPeriod: refillPeriod,
		lastRefill:   time.Now(),
	}
}

// refill adds tokens based on time elapsed since last refill
func (tb *TokenBucket) refill() {
	tb.mu.Lock()
	defer tb.mu.Unlock()

	now := time.Now()
	elapsed := now.Sub(tb.lastRefill)
	if elapsed < tb.refillPeriod {
		return
	}

	// Calculate number of refills to perform
	refills := int(elapsed / tb.refillPeriod)
	newTokens := refills * tb.refillRate
	tb.tokens = min(tb.tokens+newTokens, tb.capacity)
	tb.lastRefill = now.Add(-(elapsed % tb.refillPeriod)) // Preserve leftover time
}

// Allow checks if a token is available (blocks until token is available or context expires)
func (tb *TokenBucket) Allow(ctx context.Context) bool {
	for {
		tb.refill()

		tb.mu.Lock()
		if tb.tokens > 0 {
			tb.tokens--
			tb.mu.Unlock()
			return true
		}
		tb.mu.Unlock()

		// Check if context is done before waiting
		select {
		case <-ctx.Done():
			return false
		case <-time.After(100 * time.Millisecond): // Short sleep to prevent busy waiting
			continue
		}
	}
}

// Example usage
func main() {
	// Create rate limiter: 5 tokens capacity, 1 token per second refill rate
	limiter := NewTokenBucket(5, 1, 1*time.Second)

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	var wg sync.WaitGroup
	// Simulate 10 concurrent requests
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(requestID int) {
			defer wg.Done()
			
			start := time.Now()
			if limiter.Allow(ctx) {
				log.Printf("Request %d: allowed (waited %v)", requestID, time.Since(start))
			} else {
				log.Printf("Request %d: denied (context expired)", requestID)
			}
		}(i)
	}

	wg.Wait()
	fmt.Println("All requests processed")
}

How It Works

Controls how many operations can run per interval by consuming and refilling tokens at a steady pace.

Limiter stores capacity, refill rate, and last refill timestamp; a mutex protects shared counters; Allow refills tokens based on elapsed time then decrements if available; main demonstrates gating simulated work across goroutines.

Key Concepts

  • 1Refill logic is time-based so bursts deplete and slowly recover.
  • 2Mutex guards token calculations for concurrent callers.
  • 3Supports both single operation checks and higher-level middleware usage.
  • 4Capacity and rate are configurable to tune throughput.

When to Use This Pattern

  • Protecting APIs or database connections from spikes.
  • Throttling background jobs or cron tasks.
  • Client-side rate limiting before hitting third-party services.
  • Ensuring fairness among worker goroutines sharing a resource.

Best Practices

  • Pick capacity to allow small bursts without overwhelming backends.
  • Record rejection metrics to tune limits over time.
  • Share one limiter per resource rather than per request.
  • Guard state with a mutex; atomics alone can miss refill math.
Go Version1.13
Difficultyintermediate
Production ReadyYes
Lines of Code99