Содержание урока · 10 разделов
Одиннадцатый урок оставил direct там, где меток немного и все известны. Первые недели хватает: email, push, sms. Потом продюсер хочет различать заказы и платежи, потом города, потом severity — и ключи размножаются: order.email, payment.email, order.email.moscow. Каждая новая грань данных требует новых привязок на стороне всех подписчиков, и рано или поздно кто-то забывает добавить ключ, а потребитель молча перестаёт получать часть потока. Topic exchange решает это сменой правила сравнения: привязка становится шаблоном, и одна маска покрывает целое семейство ключей. Потребитель подписывается на «все события заказов» одной привязкой order.#, продюсер продолжает уточнять ключ сколько угодно — маршрут переживает рост. Промышленные примеры вокруг: нотификации фильтруют user., мониторинг слушает .error, мультирегиональные сервисы режут поток по суффиксу региона. В этом уроке разбираем синтаксис, звёздочки и решётки, ошибки масок и то место, где гибкость превращается в нечитаемую топологию.
Topic exchange и ключ из слов#
Ключ в topic exchange — это слова, разделённые точками: order.created, user.login.mobile, report.sales.eu.weekly. Брокер словарь не навязывает: это строки до 255 байт, точки — разделители, а согласованность слов — контракт между издателями и подписчиками. Хорошая практика: от общего к частному — домен, сущность, действие, уточнения: order.created.eu или order.cancelled.reason.timeout. Обменник объявляется третьим типом из четырёх: ch.ExchangeDeclare("events", "topic", true, false, false, false, nil) — durable, идемпотентность и PRECONDITION_FAILED при смене типа работают как у direct из одиннадцатого урока. Привязка связывает очередь с шаблоном: QueueBind("audit", "#", "events", false, nil). В management UI topic-обменник узнаётся по полю routing key в форме тестовой публикации: вбиваете реальный ключ вроде order.created и видите список очередей, куда ушла копия.
Звезда и решётка: правила шаблонов#
Шаблонов два, и в них путаются, пока не проговоришь правила словами. Звёздочка заменяет ровно одно слово: шаблон order. совпадёт с order.created и order.cancelled, но не с order.created.eu — на месте звёздочки стоит одно слово, а их два. Решётка # заменяет ноль, одно или несколько слов: order.# накрывает order.created, order.created.eu и даже order.cancelled.reason.timeout. Составные шаблоны комбинируют: *.created ловит order.created и user.created, но не report.created.eu. Крайние случаи полезно помнить: одиночный # — это fanout, он пропустит всё; шаблон без звёздочек и решёток — это direct, точное совпадение. То есть topic при желании эмулирует обоих соседей, и в маленьких проектах этим пользуются, чтобы не плодить обменники. Регистр, как и в direct, учитывается: Order.Created и order.created — разные ключи.
Три подписки на один поток событий
GOpackage main
import (
"context"
"log"
"os"
"os/signal"
amqp "github.com/rabbitmq/amqp091-go"
)
type sub struct {
queue string
mask string
}
func main() {
conn, err := amqp.Dial("amqp://guest:guest@localhost:5672/")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
ch, err := conn.Channel()
if err != nil {
log.Fatal(err)
}
err = ch.ExchangeDeclare("events", "topic", true, false, false, false, nil)
if err != nil {
log.Fatal(err)
}
subs := []sub{
{queue: "audit.all", mask: "#"},
{queue: "email.orders", mask: "order.*"},
{queue: "analytics.users", mask: "user.#"},
}
for _, s := range subs {
if _, err := ch.QueueDeclare(s.queue, true, false, false, false, nil); err != nil {
log.Fatal(err)
}
if err := ch.QueueBind(s.queue, s.mask, "events", false, nil); err != nil {
log.Fatal(err)
}
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
msgs, err := ch.ConsumeWithContext(ctx, "email.orders", "email-worker",
false, false, false, false, nil)
if err != nil {
log.Fatal(err)
}
for d := range msgs {
log.Printf("маршрут %s: %s", d.RoutingKey, d.Body)
if err := d.Ack(false); err != nil {
log.Println("ack failed:", err)
}
}
}Три подписки на один поток: аудит забирает всё через решётку, письма — события заказов первого уровня через order.*, аналитика — всё про пользователей через user.#. Delivery несёт RoutingKey — реальный ключ публикации, по нему удобно строить счётчики и логи. Публикация user.login.mobile уйдёт в audit.all и analytics.users и минует email.orders.
Попадания шаблонов: привязка против ключа#
| Привязка | order.created | order.created.eu | user.login |
|---|---|---|---|
| order.* | попадание | мимо: два слова на звёздочке | мимо |
| order.# | попадание | попадание | мимо |
| *.created | попадание | мимо | мимо |
| # | попадание | попадание | попадание |
| order.created | попадание | мимо: точное сравнение | мимо |
Кейс: подписчик отчётов привязал order.*, ожидая все события заказов, а городская выгрузка слала ключи order.created.eu и order.paid.eu — отчёты собирали долю потока и молчали. Звёздочка замещает ровно одно слово, а не «что угодно после префикса»; для ветвящейся иерархии нужна решётка: order.#. Второй подводный камень — решётка в начале шаблона: привязки вида #.paid вынуждают брокер проверять ключи по-особенному и на больших объёмах дорожают; рекомендация документации — оставлять решётку в хвосте шаблона. Третья ловушка — пустые слова: ключ order..paid с двумя точками подряд не совпадёт с order.paid, а заметить двойную точку в логах труднее, чем опечатку. Проверяйте маски тестовыми публикациями с реальными ключами, а не только с идеальными.
Rabbitmq routing: дизайн ключей и контракт команд#
Гибкость rabbitmq routing перенесла сложность с топологии на словарь: команды договариваются о словах ключа, и без реестра не обойтись. Работающая схема: издатель владеет префиксом своего домена — заказы пишут order., платежи payment., — а подписчики выбирают маски, не спрашивая издателя. Ключ не должен нести данные, которые живут в теле: user.id.1042 создаёт бесконечное множество привязок, идентификатор едёт в payload из пятнадцатого урока. Точки-разделители образуют иерархию, и важно не ссыпать в ключ все грани сразу: user.login.mobile.eu говорит и о канале, и о регионе — при добавлении новой грани чужие маски могут молча сломаться, как в warn-врезке. Словарь меняют аддитивно: новое слово — в конец ключа, старые не переименовывают — вместо этого публикуется новый ключ, и маски переводят постепенно. Delivery.RoutingKey помогает контролю: по нему собирают счётчики потока и ловят незнакомые префиксы. Когда словарь растёт быстрее, чем команды успевают договориться, выручает реестр событий с примерами ключей: он же становится документацией для новичка и входом тестов на маски.
Издатель уточняет ключ, привязки не меняются
GOctx := context.Background()
events := []string{
"order.created",
"order.created.eu",
"user.login.mobile",
"report.sales.weekly",
}
for _, key := range events {
err := ch.PublishWithContext(ctx, "events", key, false, false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte("payload of " + key),
DeliveryMode: amqp.Persistent,
})
if err != nil {
log.Fatal(err)
}
}Издатель уточняет ключ свободно: грань региона появилась во второй строке, и привязки менять не пришлось — order.# уже её ловит, а order.* честно пропустит. Так и растёт поток событий: уточнение ключей не ломает старых подписчиков, если они выбирали маски, а не точные ключи.
Topic или direct: цена гибкости#
Тема не бесплатна: сопоставление ключа с шаблоном дороже точного сравнения, и брокер выполняет его для каждой привязки на каждое сообщение; на единицах очередей это незаметно, на сотнях активных масок с ветвистыми шаблонами — уже метрика. Direct выигрывает скоростью и читаемостью: множество ключей конечно и видно в UI целиком. Спорное наблюдение из ревью: гибкость topic провоцирует на маски ради масок — привязка вида .paid..error читается хуже, чем честный список ключей direct, и разбор такой топологии превращается в разгадывание. Критерий простой: если потребители выбирают срезы потока с иерархией — topic оправдан; если набор меток стабилен и конечен — direct честнее. Поглощение соседей — ещё одна ловушка: topic умеет всё, но один обменник на все случаи жизни смешивает потоки с разной судьбой, а сменить тип у обменника потом нельзя; чаще выигрывают несколько обменников под разные классы трафика. Проверка простая: выпишите свои маски и подставьте три реальных ключа из продакшн-логов; если половина совпадений мимо или маски дублируют друг друга, топология просит упрощения.
Проверка маршрутизации на живом брокере#
Топологию topic проверяют руками быстрее, чем чтением кода. На странице обменника в management UI есть форма Publish message: вбейте routing key и тело, нажмите publish — и в списке привязок видно, куда ушла копия. Привязки каждой очереди перечислены на её странице в секции Bindings, а консольный rabbitmqctl list_bindings показывает таблицу «обменник — очередь — ключ» целиком: удобно для выгрузки и ревью. На больших схемах заводят подзорную очередь с привязкой # и читают её как дамп потока: видно всё, что публикуется, включая ключи, о которых подписчики не знали. Из той же практики — счётчик по Delivery.RoutingKey на потребителе: внезапно появившийся префикс означает, что издатель расширил словарь, и пора пересматривать маски, пока кто-то не потерял часть потока молча.
Маршрутизация по шаблонам живёт в брокере, а песочнице доступны только fmt, strings, strconv, math, sort, errors и time — без сети, поэтому код урока запускается локально против Docker-контейнера из второго урока. Почувствовать правила звёздочки и решётки можно и в песочнице: разбейте ключ strings.Split по точкам и сравните количество слов с маской — этого хватает для тренировки. Интерактивы ниже проверят шаблоны, дедупликацию копий и порядок сборки топологии.
Правила topic#
- Ключ — слова через точки; словарь слов — контракт между издателем и подписчиками.
- Звёздочка заменяет ровно одно слово, решётка — ноль или больше; решётку оставляют в хвосте шаблона.
- Одиночный # ведёт себя как fanout, шаблон без масок — как direct.
- Издатель уточняет ключ свободно, подписчики выбирают маски; новые слова добавляют в конец.
- Ключ не дублирует payload: идентификаторы и суммы живут в теле сообщения.
- Гибкость стоит сопоставления на каждой доставке: без иерархии берите direct.
Противовесы этой гибкости: direct из одиннадцатого урока с точным сравнением и fanout из двенадцатого с полной рассылкой — topic их поглощает, но не заменяет по читаемости. Ключи без тела — половина контракта: пятнадцатый урок про JSON расскажет о формате payload, а шестнадцатый про dead letter — куда уходит то, что подписчики не осилили. Дизайн ключей перекликается с потоками Redis: там тоже журнал событий с явными именами потоков. Публикация с ключами осталась в шестом уроке, мёртвые привязки и прочие антипаттерны — в двадцать третьем; карта курса — на странице курса.
Проверка: звезда и решётка
Очередь привязана к topic-обменнику с шаблоном order.*. Какой ключ до неё дойдёт?
Проверка: крайние случаи
К topic-обменнику привязаны две очереди: одна с шаблоном #, вторая — с точным ключом user.login. Публикация с ключом user.login. Что произойдёт?
Сколько доставок получит очередь?
Прочитайте код и выберите, что он выведет:
// topic exchange "events"
// одна очередь reports с двумя привязками: "*.paid" и "order.#"
published := []string{
"order.paid",
"order.paid.eu",
"cart.paid",
"order.created",
}
fmt.Println("доставок:", countDeliveries(published))Соберите привязки topic
Допишите объявление topic-обменника и две привязки подписчика.
Соберите издателя topic
Расставьте строки в порядке выполнения: соединение, канал, обменник, очередь, привязка с маской, публикация.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: topic и маски
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Topic сравнивает ключ с шаблоном: звёздочка — одно слово, решётка — хвост любой длины.
- Одиночный # эквивалентен fanout, точный шаблон — direct: topic покрывает соседей.
- Словарь ключей — контракт: издатель владеет префиксом домена, подписчики выбирают маски.
- Новые грани добавляют словами в конец ключа; переименование ломает чужие маски молча.
- Сообщение с несколькими совпавшими привязками одной очереди приходит один раз.
- Маски проверяют тестовой публикацией в UI и счётчиком по Delivery.RoutingKey.
- Без иерархии берите direct: сопоставление шаблонов стоит на каждой доставке.
Домашнее задание
Соберите topic-обменник events и три очереди с привязками #, order.* и user.#. Публикуйте ключи order.created, order.created.eu, user.login.mobile и report.sales.weekly и сверьте в management UI, сколько копий получила каждая очередь. Затем отправьте order.paid.eu и убедитесь, что order.* её пропустил, а после замены маски на order.# подписка догнала поток. В конце заведите подзорную очередь с # и найдите в ней ключ, о котором ваши подписчики не знали.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему привязка order.* не ловит order.created.eu?
Звёздочка замещает ровно одно слово: на её месте стоит created, а хвосту .eu маски некуда деться. Для ветвящейся иерархии берите решётку order.# — она покрывает и одно, и несколько слов после префикса. Проверить себя быстрее всего тестовой публикацией на странице обменника в management UI.
Придёт ли сообщение дважды, если оно совпало с двумя привязками одной очереди?
Нет: дедупликация работает на пару «очередь и сообщение» — RabbitMQ доставит одну копию независимо от числа совпавших привязок. По копии получат две разные очереди с одинаковыми шаблонами; внутри одной очереди совпадения схлопываются.
Стоит ли завести один topic-обменник на весь проект?
Часто да: один поток событий и общий словарь ключей проще держать в голове, а подписчики выбирают маски сами. Но тип обменника сменить нельзя, и смешивать всё подряд не обязательно: трафик с разной судьбой — задачи воркеров и события для аналитики — живёт лучше в отдельных обменниках со своими требованиями к durable и объёму.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Headers и альтернативный exchange
- Формат сообщений: JSON и структуры
- Ошибки потребителя: retry и dead letter
- Паттерн worker pool на очереди
- Fanout: рассылка всем подписчикам
- Обменники: direct exchange и routing key
- Prefetch и fair dispatch: QoS потребителя
- Долговечность: durable и persistent
- Все курсы по Go
Следующая ступень учебного пути
Excel в Go: чтение и генерация xlsx на excelize
Курс excelize на Go: создание xlsx, чтение, стили, формулы, диаграммы и StreamWriter для больших файлов. 25 уроков с примерами кода и проектом.
- Создавать и читать xlsx-файлы средствами excelize
- Работать с листами, ячейками, строками и столбцами
- Оформлять таблицы: стили, шрифты, границы, форматы чисел и дат
- Вставлять формулы, диаграммы и изображения