Por que sua aplicação Go está lenta? Provavelmente você esqueceu o cache
Você escreveu a aplicação perfeita em Go. Se orgulha das goroutines, canais e do compilador rápido. Mas quando a carga atinge 1000 RPS, tudo começa a ranger. O banco de dados cai, a API responde em 5 segundos, os usuários vão para os concorrentes.
Situação familiar? Bem-vindo ao mundo onde cache em Go é indispensável. Neste artigo, vamos ver como transformar sua aplicação tartaruga em um guepardo. Sem enrolação, com exemplos reais e soluções prontas.
A propósito, meu nome é Python, e este é o PythonLib. Sim, eu sei que estamos falando de Go. Mas código bom não conhece fronteiras de linguagens, e escrevemos artigos úteis sobre tudo. Vamos nessa!
O que é cache e por que você precisa dele em Go?
Cache de dados é o armazenamento de dados frequentemente solicitados em um repositório rápido (memória RAM, Redis, arquivos) para não consultar a fonte lenta (banco de dados, API externa, sistema de arquivos) toda vez.
Go é uma linguagem para sistemas de alta carga. Mas mesmo em Go você pode escrever código lento se não usar cache. Imagine: você consulta a lista de países do PostgreSQL 1000 vezes por segundo. Em vez de 1000 consultas ao banco, você pode fazer apenas 1, e as outras 999 vezes entregar os dados do cache in-memory Go.
Resultado: o tempo de resposta cai de 50 ms para 0.5 ms. Uma diferença de 100 vezes. Isso não é mágica, é arquitetura bem planejada.
Quando o cache é obrigatório?
- Alta carga (1000+ requisições por segundo)
- APIs externas lentas (pagamentos, clima, taxas de câmbio)
- Cálculos pesados (relatórios, análises)
- Dados de referência frequentemente solicitados (listas de cidades, categorias de produtos)
Estratégias de cache: escolhendo a correta
Para não transformar o cache em antipadrão, você precisa escolher a estratégia de cache correta. Aqui estão as principais opções que funcionam em Go:
1. Cache-Aside (cache preguiçoso)
O padrão mais popular. A aplicação primeiro verifica o cache. Se os dados não estiverem lá (cache miss) — vai ao banco, salva o resultado no cache e retorna ao cliente.
func GetUser(cache *redis.Client, db *sql.DB, userID int) (*User, error) { // Tenta obter do cache val, err := cache.Get(ctx, "user:"+strconv.Itoa(userID)).Result() if err == nil { user := &User{} json.Unmarshal([]byte(val), user) return user, nil }
// Cache miss — vai ao banco user, err := db.Query("SELECT * FROM users WHERE id = ?", userID) if err != nil { return nil, err }
// Salva no cache por 1 hora data, _ := json.Marshal(user) cache.Set(ctx, "user:"+strconv.Itoa(userID), data, time.Hour)
return user, nil}2. Write-Through (escura direta)
Os dados são escritos simultaneamente no cache e no banco. Vantagem: o cache está sempre atualizado. Desvantagem: a escrita é um pouco mais lenta.
3. TTL (Time To Live)
A maneira mais simples de invalidação de cache. Os dados vivem por um tempo fixo e depois são removidos automaticamente.
// Cache por 5 minutoscache.Set(ctx, "key", value, 5*time.Minute)Implementação de cache in-memory em Go do zero
Às vezes você não quer subir um Redis só para algumas chaves. Para esses casos, o cache in-memory Go é perfeito. Vamos escrever uma versão simples, mas funcional:
package cache
import ( "sync" "time")
type Item struct { Value interface{} Expiration int64}
type Cache struct { items map[string]Item mu sync.RWMutex}
func New() *Cache { c := &Cache{ items: make(map[string]Item), } // Inicia goroutine para limpar entradas expiradas go c.cleanup() return c}
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) { c.mu.Lock() defer c.mu.Un