¿Por qué tu aplicación Go va lenta? Seguramente olvidaste el caché
Escribiste la aplicación perfecta en Go. Estás orgulloso de las gorutinas, los canales y el compilador rápido. Pero cuando la carga alcanza las 1000 RPS, todo empieza a crujir. La base de datos se cae, la API responde en 5 segundos, los usuarios se van a la competencia.
¿Te suena familiar? Bienvenido al mundo donde no puedes prescindir del caché en Go. En este artículo veremos cómo convertir tu aplicación tortuga en un guepardo. Sin rodeos, con ejemplos reales y soluciones listas.
Por cierto, me llamo Python, y esto es PythonLib. Sí, sí, sé que estamos hablando de Go. Pero el buen código no conoce límites de idiomas, y escribimos artículos útiles sobre todo. ¡Vamos allá!
¿Qué es el caché y para qué sirve en Go?
El caché de datos es guardar datos solicitados con frecuencia en un almacenamiento rápido (memoria RAM, Redis, archivos) para no tener que consultar cada vez la fuente lenta (base de datos, API externa, sistema de archivos).
Go es un lenguaje para sistemas de alta carga. Pero incluso en Go se puede escribir lento si no se usa caché. Imagina: solicitas 1000 veces por segundo la lista de países desde PostgreSQL. En lugar de 1000 consultas a la BD, puedes hacer solo 1, y las otras 999 veces devolver los datos desde el caché en memoria Go.
Resultado: la velocidad de respuesta baja de 50 ms a 0.5 ms. Una diferencia de 100 veces. No es magia, es una arquitectura bien diseñada.
¿Cuándo es obligatorio el caché?
- Alta carga (1000+ solicitudes por segundo)
- APIs externas lentas (pagos, clima, tipos de cambio)
- Cálculos pesados (informes, análisis)
- Datos de referencia solicitados con frecuencia (listas de ciudades, categorías de productos)
Estrategias de caché: elige la correcta
Para no convertir el caché en un antipatrón, debes elegir la estrategia de caché adecuada. Estas son las principales opciones que funcionan en Go:
1. Cache-Aside (caché perezoso)
El patrón más popular. La aplicación primero verifica el caché. Si no hay datos (cache miss), va a la BD, guarda el resultado en el caché y lo devuelve al cliente.
func GetUser(cache *redis.Client, db *sql.DB, userID int) (*User, error) { // Intentamos obtener del caché val, err := cache.Get(ctx, "user:"+strconv.Itoa(userID)).Result() if err == nil { user := &User{} json.Unmarshal([]byte(val), user) return user, nil }
// Fallo de caché — vamos a la BD user, err := db.Query("SELECT * FROM users WHERE id = ?", userID) if err != nil { return nil, err }
// Guardamos en caché por 1 hora data, _ := json.Marshal(user) cache.Set(ctx, "user:"+strconv.Itoa(userID), data, time.Hour)
return user, nil}2. Write-Through (escritura directa)
Los datos se escriben simultáneamente en el caché y en la BD. Ventaja: el caché siempre está actualizado. Desventaja: la escritura es un poco más lenta.
3. TTL (Time To Live)
La forma más sencilla de invalidación de caché. Los datos viven un tiempo fijo y luego se eliminan automáticamente.
// Caché por 5 minutoscache.Set(ctx, "key", value, 5*time.Minute)Implementación de un caché en memoria en Go desde cero
A veces no quieres levantar Redis solo para un par de claves. Para estos casos, el caché en memoria Go es perfecto. Escribamos una versión simple pero 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), } // Iniciamos una gorutina para limpiar entradas vencidas go c.cleanup() return c}
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) { c.mu.Lock() defer c.mu.Un