Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance


🗺️ Mapa Conceitual do Tópico

flowchart TD
    A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
    B --> C["Controller / Handler"]
    C --> D["Service Layer (Regras de Negócio)"]
    D --> E["Repository / ORM (Persistência)"]
    E --> F["Banco de Dados / Cache"]

    subgraph ARQ["Arquitetura do Capítulo"]
        G["Conceito: Rate Limiting com Token Bucket e Proteção de API"]
        H["Segurança, Validação e Resiliência"]
        I["Alta Performance e Escalabilidade"]
    end

    D --> ARQ

    style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
    style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
    style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
    style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px

🏛️ 1. Fundamentos Técnicos de Rate Limiting com Token Bucket e Proteção de API

Rate limiting existe para proteger a API contra dois cenários muito diferentes: o cliente legítimo que erra (um loop de retry sem backoff) e o cliente malicioso (scraping agressivo, força bruta em login, DDoS de aplicação). O algoritmo mais usado em produção Go é o Token Bucket, implementado pela biblioteca oficial golang.org/x/time/rate: um “balde” acumula tokens a uma taxa constante (r tokens/segundo) até um limite (burst); cada requisição consome um token, e se o balde estiver vazio a requisição é rejeitada com 429 Too Many Requests. A vantagem sobre um contador fixo por janela é que o Token Bucket absorve picos legítimos (o burst) sem abrir mão do controle da taxa média sustentada — um cliente que enviou 3 requisições rápidas e depois ficou ocioso não é penalizado como se estivesse martelando o servidor.

limiter := rate.NewLimiter(5, 10) // 5 req/s sustentado, burst de até 10
if !limiter.Allow() {
    // rejeitar com 429
}

Em Gin, o padrão idiomático é manter um *rate.Limiter por chave de identificação do cliente (tipicamente c.ClientIP(), ou o ID do usuário autenticado quando disponível), guardado em um map[string]*rate.Limiter protegido por sync.Mutex — um único limiter global penalizaria todos os clientes pelo comportamento de um só. Esse mapa em memória tem uma limitação importante: não é compartilhado entre instâncias da API. Em um deployment com múltiplas réplicas atrás de um load balancer, cada instância aplica seu próprio limite independente, então o limite efetivo percebido pelo cliente é limite × número de réplicas. A solução é externalizar o estado para o Redis, usando INCR sobre uma chave por IP/janela de tempo seguido de EXPIRE para expirar a janela — isso implementa uma sliding/fixed window coordenada entre todas as instâncias, ao custo de uma chamada de rede por requisição (mitigável com pipelining ou scripts Lua atômicos).

Rate limiting é só uma camada de uma postura de segurança defensiva mais ampla:

  • CORS (cors.New() do pacote github.com/gin-contrib/cors): controla, do lado do navegador, quais origens podem chamar a API via JavaScript. Não é proteção contra chamadas server-to-server ou curl — é uma política que os navegadores respeitam, não o servidor.
  • Headers de segurança HTTP: Strict-Transport-Security (força HTTPS em conexões futuras), X-Content-Type-Options: nosniff (impede que o navegador reinterprete o Content-Type), X-Frame-Options: DENY (bloqueia clickjacking via <iframe>) e Content-Security-Policy (restringe de onde scripts/estilos podem ser carregados).
  • Timeouts no http.Server: o padrão http.ListenAndServe() sem configuração explícita não tem ReadTimeout, WriteTimeout nem IdleTimeout — um cliente que abre a conexão e envia bytes lentamente (ataque Slowloris) mantém a goroutine do handler presa indefinidamente, esgotando o pool de conexões. Configurar esses timeouts explicitamente no http.Server é obrigatório em produção.
  • Blacklisting dinâmico: contabilizar erros 4xx (especialmente 401/403) por IP e bloquear temporariamente quem ultrapassa um limiar é uma defesa simples e eficaz contra brute-force e scanners automatizados, geralmente combinada com uma janela de expiração (ex.: 15 minutos) para não punir permanentemente por um erro isolado.

É importante escolher o código de resposta certo: 429 para rate limit excedido (idealmente acompanhado do header Retry-After), 403 para bloqueio de política/blacklist, e nunca 500 — esses são recusas deliberadas do servidor, não falhas internas.


💻 2. Código de Demonstração Corporativo

package middlewares

import (
    "net/http"
    "sync"

    "github.com/gin-gonic/gin"
    "golang.org/x/time/rate"
)

// LimiterPorIP mantém um Token Bucket independente por endereço IP, evitando
// que o tráfego de um cliente consuma a cota de todos os demais.
type LimiterPorIP struct {
    mu       sync.Mutex
    limiters map[string]*rate.Limiter
    r        rate.Limit
    burst    int
}

func NovoLimiterPorIP(reqsPorSegundo float64, burst int) *LimiterPorIP {
    return &LimiterPorIP{
        limiters: make(map[string]*rate.Limiter),
        r:        rate.Limit(reqsPorSegundo),
        burst:    burst,
    }
}

func (l *LimiterPorIP) obter(ip string) *rate.Limiter {
    l.mu.Lock()
    defer l.mu.Unlock()
    lim, existe := l.limiters[ip]
    if !existe {
        lim = rate.NewLimiter(l.r, l.burst)
        l.limiters[ip] = lim
    }
    return lim
}

func (l *LimiterPorIP) Middleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        if !l.obter(c.ClientIP()).Allow() {
            c.Header("Retry-After", "1")
            c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"erro": "limite de requisições excedido"})
            return
        }
        c.Next()
    }
}