Кэширование в Go: как ускорить приложение в 100 раз и не сойти с ума

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему ваше 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
    }

    // Кэш промах — идём в Б                

Блоги

Рекомендаций по книгам