---
title: "Типы данных Redis: обзор под задачи Go"
description: "Типы данных redis: строки, хэши, списки, множества и ZSET — какой тип брать под очередь, профиль и рейтинг в Go. Таблица выбора и команды go-redis."
course: "Redis в Go: кэш, сессии и очереди на go-redis"
url: "https://golangmaster.ru/courses/go-redis/lessons/03-tipy-dannyh-redis/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Типы данных Redis: обзор под задачи Go

*Курс «Redis в Go: кэш, сессии и очереди на go-redis», урок 3 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-redis/lessons/03-tipy-dannyh-redis/*

Типы данных redis — то, что отличает его от плоского кэша ключ-значение: строки, хэши, списки, множества и ZSET закрывают задачи от профиля пользователя до лидерборда без сторонних сервисов. В этом уроке — карта пяти типов под задачи Go-проекта, по одной команде на каждый и ошибка WRONGTYPE, которая ждёт всех, кто перепутает типы. Копать глубоко не будем: у каждого типа впереди собственный урок.

Пересаживаясь с Go на Redis, легко ожидать, что значения будут вести себя как структуры и срезы. Ожидание обманывает: в Redis всё хранится по схеме ключ — тип — значение, и тип закреплён за ключом жёстко. Положили под ключ user:42 строку — это навсегда строковый ключ, пока не удалите его: команда LPUSH по нему вернёт ошибку WRONGTYPE, а не тихо конвертирует значение. Именно поэтому выбор типа — первое решение при проектировании Redis-схемы, и делается оно не по вкусу, а под характер доступа: что читаем чаще, обновляем ли поля по отдельности, нужен ли порядок и сколько должно жить значение. Хорошая новость в том, что типов всего пять с половиной, и каждый закрывает понятный класс задач. Ниже — обзор каждого с командами go-redis, таблица-шпаргалка в середине и правило выбора в конце. Код урока запускается локально: песочнице сайта недоступна сеть, а ваш Redis в Docker из второго урока уже готов принимать команды.

### Типы данных Redis: карта для Go-разработчика

Пять базовых типов — строки, хэши, списки, множества и сортированные множества — плюс шестой, Streams, для журналов событий. Строки — универсальный дефолт: кэш ответа, счётчик, флаг, готовый JSON. Хэши хранят объект как карту поле-значение и позволяют менять одно поле, не трогая остальные. Списки — упорядоченные коллекции с быстрыми операциями по краям, из них собирают очереди задач. Множества хранят уникальные значения без порядка и умеют пересечения и разности. ZSET — то же множество, но у каждого члена есть числовой score, и выборка идёт по диапазону очков: отсюда лидерборды и тренды. Streams — журнал с чтением по позициям и группами потребителей, старший брат списков для серьёзных пайплайнов. Таблица ниже — шпаргалка, к которой стоит возвращаться при проектировании схемы: она отвечает на вопрос «каким типом хранить» за пару секунд.

### Пять типов и одна задача на каждый

| Тип | Что хранит | Команды | Задача в Go-сервисе |
| --- | --- | --- | --- |
| String | строка, число, JSON, байты | SET, GET, INCR | кэш ответа, счётчик, флаг |
| Hash | карта поле-значение | HSet, HGet, HGetAll | профиль пользователя, состояние объекта |
| List | упорядоченный список строк | LPush, RPop, BRPop | очередь задач |
| Set | уникальные значения без порядка | SAdd, SIsMember, SInter | уникальные посетители, теги, права |
| ZSET | члены с числовым score | ZAdd, ZRevRange, ZIncrBy | лидерборд, тренды, скользящее окно |
| Stream | журнал записей с ID | XAdd, XRead | лог событий, надёжная очередь |

### Строки: базовый тип для кэша и счётчиков

Строка — единственный тип, который нужен для классического кэша: SET кладёт значение, GET достаёт. Называть её строкой честно только с натяжкой: значение может быть текстом, числом, JSON-документом или произвольными байтами — Redis бинарно-безопасен, а потолок одного значения — 512 мегабайт, до которого почти никто не добирается. Числовые строки получают суперспособность: INCR и DECR атомарно меняют значение, поэтому счётчик просмотров — это строка с числом, и никакой мьютекс не нужен. В go-redis метод Set принимает значение как интерфейс, так что положить можно строку, int, float или слайс байт; структуры сериализуют в JSON руками — этому посвящён отдельный урок про кэш структур. Строки — правильный дефолт: сомневаетесь, каким типом хранить, — кладите строку и усложняйте, только когда упрётесь в задачу.

### Хэши, списки и множества: объекты и очереди

Хэш — карта внутри ключа: HSet пишет поля, HGet читает одно, HGetAll возвращает всё сразу. Профиль пользователя — канонический пример: имя, возраст и статус обновляются по отдельности, хэш экономит трафик и не даёт затереть соседнее поле, как случилось бы с перезаписью целой строки. В Go значения полей — обычные аргументы через запятую или готовая map. Список — упорядоченная коллекция с быстрыми операциями по краям: LPush кладёт в голову, RPop забирает из хвоста, и на этой паре держится простейшая очередь задач — продюсер кладёт, воркер забирает. Блокирующий BRPop усыпляет воркер до появления задачи вместо дежурного опроса пустого списка. Множество — набор уникальных значений без порядка: SAdd добавляет, дубликат тихо игнорируется, SIsMember проверяет наличие за константное время, SInter находит пересечение двух множеств. Уникальные посетители дня, теги статьи, права пользователя — всё это множества. Для сопоставления с Go: hash — map, list — двусторонняя очередь, set — map с пустыми значениями.

### ZSET: рейтинги, лидерборды и окна времени

Сортированное множество — визитная карточка Redis. Каждый член хранится с числовым score, и структура держит порядок по нему постоянно: ZAdd записывает очки, ZIncrBy добавляет к существующим, ZRevRange вытаскивает топ N, ZRank отвечает, на каком месте участник. Лидерборд игры собирается тремя строками кода, и тот же механизм обслуживает тренды: score — число просмотров за час, member — идентификатор статьи. Второе применение — временные окна: score как timestamp превращает ZSET в скользящее окно для rate limiter — добавляем время каждого запроса, чистим устаревшее, считаем размер. В Go-коде запись выглядит как структура redis.Z с полями Score: 1500 и Member: user:42 — score всегда float64, поэтому большие целые идентификаторы в score не кладут — им место в member, а score остаётся метрикой.

> **WRONGTYPE: тип закреплён за ключом.** Самая обидная ошибка первых недель: ключ создан через SET, а вы зовёте LPUSH — сервер отвечает WRONGTYPE Operation not permitted. Redis не конвертирует типы: за ключом закреплена одна структура, и чтобы переиспользовать имя, ключ надо удалить через DEL. Лечится конвенцией имён: префикс отражает назначение — counter:, queue:, board:, user: — и коллизии не случается. В go-redis ошибка приходит обычным err из Result, без паник и особых сценариев.

### Кейс из продакшена: WRONGTYPE после смены типа

История из реального проекта: корзину хранили строкой с JSON, в релизе переехали на хэш — удобнее менять поля по отдельности. Новый код записывает HSet, а старый воркер отложенных заказов по-прежнему зовёт GET по тому же ключу — и тут тонкость: GET по хэшу сервер вернёт ошибку WRONGTYPE ровно так же, как LPUSH по строке. Воркер стал падать на каждом заказе, очередь копилась, пользователи не получали подтверждений — классический инцидент из-за смены типа под живым ключом. Разбор дал два урока. Первый: тип ключа — часть контракта, и менять его можно только с миграцией, которая переименовывает ключи или переливает данные под новым именем. Второй: конвенция префиксов в имени — cart:, cart:v2: — дешёвый способ развести версии схемы и развести старый и новый код. WRONGTYPE в логах после релиза ищите сразу: это почти всегда переходный период смены схемы.

### Как выбрать тип под задачу

Правило выбора стоит проговорить как чек-лист. Значение пишется и читается целиком — берите строку. Объект с полями, которые меняются по отдельности, — хэш. Важен порядок обработки, задачи приходят и уходят — список. Нужны уникальность и проверка наличия — множество. Нужен порядок по числовому критерию, топы и диапазоны — ZSET. Нужен журнал, который переживает чтение и распределение между воркерами, — Streams. Если задача попадает в два пункта сразу, начинайте с более простого типа: хэш вместо строки окупается, только когда полей много и они живут своей жизнью, а Streams вместо списка — когда сообщениям нужна подтверждаемая доставка. Простые типы дешевле обслуживать, а миграцию между типами в Redis не завезли.

### Шпаргалка выбора

- Кэш целиком, флаг, число — строка: SET, GET, INCR.
- Объект с независимыми полями — хэш: HSet, HGet, HGetAll.
- Очередь заданий — список: LPush, RPop, BRPop.
- Уникальность и пересечения — множество: SAdd, SIsMember, SInter.
- Рейтинг, топ, окно по времени — ZSET: ZAdd, ZRevRange.
- Журнал событий с группами потребителей — Streams: XAdd, XRead.

### Ответ на частый вопрос: сколько памяти съедает тип

Вопрос, который всплывает на планёрках по лимитам памяти: зависит ли расход от типа. Зависит, и заметно. Каждый ключ в Redis — это не только значение, но и служебные структуры вокруг него, поэтому миллион мелких строк весит ощутимо больше, чем сумма их содержимого. Мелкий хэш, список или множество сервер хранит компактной кодировкой вроде listpack — несколько полей в одном ключе обходятся дешевле, чем несколько отдельных ключей-строк. Но у компактной кодировки есть порог по числу элементов и по размеру значений: после него структура переключается на общую кодировку, и компактность уходит. Практических следствия два. Объект с десятком полей выгоднее держать хэшем, чем рассыпать по ключам user:42:name, user:42:age. А список на миллион записей — это не компактность, а нагрузка на память и на команды по краям; для таких объёмов существует [Streams](/courses/go-redis/lessons/15-potoki-streams/) и посвящённый им урок.

### Четыре типа — четыре команды

```go
// профиль пользователя целиком
rdb.HSet(ctx, "user:42", "name", "Аня", "age", 30)

// очередь: задача в конец списка
rdb.RPush(ctx, "jobs", "send-email:42")

// уникальные посетители за день
rdb.SAdd(ctx, "visitors:2024-06-01", "42")

// очки игрока в рейтинге
rdb.ZAdd(ctx, "leaderboard", &redis.Z{Score: 1500, Member: "user:42"})
```

Четыре команды — четыре типа: хэш профиля, очередь задач, множество онлайна и рейтинг. Имена ключей построены по конвенции сущность:идентификатор — она убережёт и от WRONGTYPE, и от хаоса в больших схемах. Каждый вызов возвращает команду, из которой достаётся результат; построчные разборы — в уроках про отдельные типы.

### Строка с числом: счётчик без мьютексов

```go
rdb.Set(ctx, "views:article:7", 0, 0)
rdb.Incr(ctx, "views:article:7")
rdb.Incr(ctx, "views:article:7")

n, _ := rdb.Get(ctx, "views:article:7").Result()
fmt.Println(n) // 2
```

Строки с числами поддерживают атомарную арифметику: Set положил ноль, два вызова Incr подняли счётчик до двух. Никаких гонок: команды исполняются по очереди в одном потоке сервера. Просмотры, лайки, лимиты — это ровно этот паттерн; полный разбор INCR и его применений — в уроке про счётчики.

### Проверка: лидерборд

**Вопрос:** Какой тип выбрать для топ-100 игроков по очкам?

- Строку с JSON-массивом
- Хэш
- Множество
- ✅ Сортированное множество

Топ по числовому критерию с постоянным порядком — ниша ZSET: очки в score, игрок в member, ZRevRange 0 99 отдаёт первую сотню. Хэш не умеет порядок, множество — очков, а JSON в строке придётся пересортировывать руками.

### Проверка: WRONGTYPE

**Вопрос:** Ключ создан через SET, код зовёт LPUSH по нему же. Что вернётся?

- Тип конвертируется автоматически
- ✅ Ошибка WRONGTYPE
- Список молча заменит строку
- Ключ удалится

Тип закреплён за ключом до DEL: несовместимая команда возвращает WRONGTYPE. Конвенция префиксов в именах ключей сводит такие коллизии к нулю.

### Что вернёт HGet?

```go
rdb.HSet(ctx, "user:42", "name", "Аня", "age", 30)
rdb.HSet(ctx, "user:42", "age", 31)

age, err := rdb.HGet(ctx, "user:42", "age").Result()
fmt.Println(age, err)
```

**Что выведет программа?**

- 30 <nil>
- ✅ 31 <nil>
- Аня <nil>
- ошибка WRONGTYPE

HSet перезаписывает поле: сначала age стал 30, потом 31. HGet читает ровно одно поле — вернулась последняя запись, ошибка nil. Поле name осталось нетронутым: хэш меняет значения полями, не задевая остальное.

### Подберите команды под задачу

Впишите команды: профиль пользователя, уникальные посетители, рейтинг игроков.

```go
// профиль пользователя
rdb.___(ctx, "user:42", "name", "Аня")

// уникальные посетители за день
rdb.___(ctx, "visitors:2024-06-01", "42")

// очки игрока в рейтинге
rdb.___(ctx, "leaderboard", &redis.Z{Score: 1500, Member: "user:42"})
```

Подсказка: Объект пишется HSet, уникальные значения — SAdd, очки в сортированном множестве — ZAdd.

Правильные ответы в пропусках: `HSet`, `SAdd`, `ZAdd`

### Соберите поток записи и чтения

Расставьте команды по порядку работы: контекст, клиент, две записи, чтение и вывод.

Правильный порядок строк:

1. `ctx := context.Background()`
2. `rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})`
3. `rdb.HSet(ctx, "user:42", "name", "Аня", "age", 30)`
4. `rdb.SAdd(ctx, "user:42:tags", "admin")`
5. `name, _ := rdb.HGet(ctx, "user:42", "name").Result()`
6. `fmt.Println(name)`

### Карточки: типы данных Redis

- **String** — Базовый тип: текст, число, JSON, байты. SET/GET для кэша, INCR для атомарных счётчиков. Потолок значения — 512 мегабайт.
- **Hash** — Карта поле-значение внутри ключа: HSet/HGet/HGetAll. Объект с независимыми полями — профиль, состояние.
- **List** — Упорядоченный список с быстрыми краями: LPush/RPop, блокирующий BRPop. Основа простой очереди задач.
- **Set** — Уникальные значения без порядка: SAdd, SIsMember за константу, SInter для пересечений. Посетители, теги, права.
- **ZSET** — Множество с score у каждого члена: ZAdd, ZRevRange, ZIncrBy. Лидерборды, тренды, скользящие окна.
- **WRONGTYPE** — Ошибка несовместимости: тип закреплён за ключом до DEL. Лечится конвенцией префиксов в именах ключей.

Карта типов собрана: строка — дефолт, хэш — объект, список — очередь, множество — уникальность, ZSET — порядок по очкам. Следующий урок — строки в деле: [GET, SET и первые ошибки](/courses/go-redis/lessons/04-stroki-get-set/), где соберём первый настоящий кэш и отличим redis.Nil от аварии. Дальше каждый тип получит свой урок — от [хэшей](/courses/go-redis/lessons/08-hashi/) до [лидербордов на ZSET](/courses/go-redis/lessons/13-zset-reitingi/), а полный план — на [странице курса](/courses/go-redis/). Приятная новость для тех, кто работает с базой через [GORM](/courses/gorm/): типы Redis идеально ложатся на кэширование моделей, этим займёмся в шестом уроке. Код запускается локально — Docker с Redis из второго урока всё ещё ждёт ваши команды.

## Коротко: что запомнить

- Схема Redis — ключ, тип, значение: тип закреплён за ключом до удаления.
- Пять базовых типов плюс Streams; строка — универсальный дефолт и правильная первая ставка.
- Хэш — объект с независимыми полями, список — очередь, множество — уникальность, ZSET — порядок по score.
- INCR делает числовую строку атомарным счётчиком без мьютексов.
- WRONGTYPE — сигнал несовместимой команды: лечится конвенцией имён ключей.
- score в ZSET — float64: идентификаторы живут в member, а не в score.
- Проверять типы удобно на живом Redis из Docker: песочница сети не умеет.

## Домашнее задание

Поднимите Redis из второго урока и воспроизведите таблицу типов командами go-redis: HSet по user:42, RPush в jobs, SAdd в visitors, ZAdd в leaderboard. Затем специально вызовите LPUSH по строковому ключу и рассмотрите ошибку WRONGTYPE в err. Песочница для этого не подойдёт — нужен локальный Go и запущенный Redis.

## Вопросы и ответы по уроку

### Что хранить в строке, а что в хэше?

Если значение читается и пишется целиком — строка: кэш ответа, флаг, счётчик. Если объект имеет поля, которые обновляются по отдельности или читаются выборочно, — хэш. Хэш экономит трафик на частичных чтениях, но для простых значений избыточен.

### Можно ли изменить тип существующего ключа?

Нет: тип закреплён за ключом, несовместимая команда вернёт WRONGTYPE. Переход возможен только через удаление ключа DEL и запись заново. На практике проблему предупреждают конвенцией имён: префикс отражает тип и назначение.

### Где Redis хранит Streams и почему не везде используют списки?

Streams — отдельный тип журнала: записи имеют ID, хранятся устойчиво к чтению, потребители читают по позициям и объединяются в группы. Список после RPop запись теряет, поэтому для надёжных очередей с подтверждением доставки берут Streams — им посвящён пятнадцатый урок.
