Почему ваше Go-приложение тормозит? Скорее всего, вы забыли про кэш
Вы написали идеальное приложение на Go. Гордитесь горутинами, каналами и быстрым компилятором. Но когда нагрузка достигает 1000 RPS, всё начинает скрипеть. База данных падает, API отвечает по 5 секунд, пользователи уходят к конкурентам.
Знакомая ситуация? Добро пожаловать в мир, где без кэширования в Go никуда. В этой статье мы разберём, как превратить ваше черепашье приложение в гепарда. Без воды, с реальными примерами и готовыми решениями.
Кстати, меня зовут Python, и это — PythonLib. Да-да, я знаю, что мы про Go говорим. Но хороший код не знает границ языков, а полезные статьи мы пишем обо всём. Погнали!
Что такое кэширование и зачем оно нужно в Go?
Кэширование данных — это сохранение часто запрашиваемых данных в быстром хранилище (оперативная память, Redis, файлы), чтобы не дёргать каждый раз медленный источник (базу данных, внешний API, файловую систему).
Go — язык для высоконагруженных систем. Но даже на Go можно написать медленно, если не использовать кэш. Представьте: вы 1000 раз в секунду запрашиваете из PostgreSQL список стран. Вместо 1000 запросов к БД можно сделать всего 1, а остальные 999 раз отдавать данные из in-memory cache Go.
Результат: скорость ответа падает с 50 мс до 0.5 мс. Разница в 100 раз. Это не магия, это грамотная архитектура.
Когда кэш обязателен?
- Высокая нагрузка (1000+ запросов в секунду)
- Медленные внешние API (платёжки, погода, курсы валют)
- Тяжёлые вычисления (отчёты, аналитика)
- Часто запрашиваемые справочные данные (списки городов, категории товаров)
Стратегии кэширования: выбираем правильную
Чтобы не превратить кэш в антипаттерн, нужно выбрать правильную стратегию кэширования. Вот основные варианты, которые работают в Go:
1. Cache-Aside (ленивое кэширование)
Самый популярный паттерн. Приложение сначала проверяет кэш. Если данных нет (cache miss) — идёт в БД, сохраняет результат в кэш и возвращает клиенту.
func GetUser(cache *redis.Client, db *sql.DB, userID int) (*User, error) {
// Пытаемся достать из кэша
val, err := cache.Get(ctx, "user:"+strconv.Itoa(userID)).Result()
if err == nil {
user := &User{}
json.Unmarshal([]byte(val), user)
return user, nil
}
// Кэш промахнулся — идём в БД
user, err := db.Query("SELECT * FROM users WHERE id = ?", userID)
if err != nil {
return nil, err
}
// Сохраняем в кэш на 1 час
data, _ := json.Marshal(user)
cache.Set(ctx, "user:"+strconv.Itoa(userID), data, time.Hour)
return user, nil
}
2. Write-Through (сквозная запись)
Данные пишутся одновременно и в кэш, и в БД. Плюс: кэш всегда актуален. Минус: чуть медленнее запись.
3. TTL (Time To Live)
Самый простой способ инвалидации кэша. Данные живут фиксированное время, потом автоматически удаляются.
// Кэш на 5 минут
cache.Set(ctx, "key", value, 5*time.Minute)
Реализация in-memory кэша в Go своими руками
Иногда не хочется поднимать Redis ради пары ключей. Для таких случаев отлично подходит in-memory cache Go. Напишем простой, но рабочий вариант:
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),
}
// Запускаем горутину для очистки просроченных записей
go c.cleanup()
return c
}
func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
c.mu.Lock()
defer c.mu.Unlock()
c.items[key] = Item{
Value: value,
Expiration: time.Now().Add(ttl).UnixNano(),
}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
item, found := c.items[key]
if !found {
return nil, false
}
if time.Now().UnixNano() > item.Expiration {
return nil, false
}
return item.Value, true
}
func (c *Cache) cleanup() {
for {
time.Sleep(time.Minute)
c.mu.Lock()
for k, v := range c.items {
if time.Now().UnixNano() > v.Expiration {
delete(c.items, k)
}
}
c.mu.Unlock()
}
}
Этот Golang кэш пример использует RWMutex для безопасной работы из горутин. Он не самый производительный для миллиона записей, но для 90% задач — идеален.
Redis в Go: мощь, доступная каждому
Если данных много или приложение распределённое, без Redis не обойтись. Кэширование в Go с Redis — стандарт индустрии. Подключается за 5 минут:
import (
"github.com/go-redis/redis/v8"
"context"
)
var ctx = context.Background()
func main() {
rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // no password set
DB: 0, // use default DB
})
// Пишем в кэш
err := rdb.Set(ctx, "key", "value", 0).Err()
if err != nil {
panic(err)
}
// Читаем из кэша
val, err := rdb.Get(ctx, "key").Result()
if err != nil {
panic(err)
}
fmt.Println("key", val)
}
Redis поддерживает сложные структуры данных: списки, хеши, сортированные множества. Это позволяет оптимизировать производительность Go даже для нетривиальных кейсов.
Продвинутый пример: кэширование с Redis + JSON
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}
func GetProduct(cache *redis.Client, db *sql.DB, id int) (*Product, error) {
// Пробуем достать из Redis
data, err := cache.Get(ctx, "product:"+strconv.Itoa(id)).Bytes()
if err == nil {
product := &Product{}
json.Unmarshal(data, product)
return product, nil
}
// Кэш промах — идём в Б