Содержание урока · 10 разделов
- Отправка сообщений rabbitmq: сигнатура PublishWithContext
- Что вернёт брокер: nil не значит доставлено
- amqp.Publishing: тело и заголовки сообщения
- Поля amqp.Publishing на каждый день
- mandatory и NotifyReturn: возврат вместо молчания
- Контекст, горутины и размер тела
- Правила публикации, которые экономят ночи
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Очередь из пятого урока объявлена и ждёт. Кажется, осталось заменить чтение на запись — и конвейер работает. Но публикация — самое обманчивое место брокера: продюсер получает nil почти в любых обстоятельствах и привыкает считать это подтверждением, которого нет. Отправьте сообщение в очередь, которой не существует, — вернётся nil. Опубликуйте в обменник без единой привязки — nil. Уроните сеть в момент отправки — результат зависит от того, успел ли кадр уйти в сокет. Баги публикации обнаруживаются не по ошибкам, а по жалобам: письма не уходят, отчёты не строятся, а логи чисты, потому что оповестить об отказе попросту некому. Этот урок про то, что именно проверяет брокер при отправке сообщений rabbitmq, что он возвращает продюсеру и как сделать так, чтобы недоставленное сообщение нашлось, а не растворилось в тишине.
Отправка сообщений rabbitmq: сигнатура PublishWithContext#
Сигнатура ch.PublishWithContext(ctx context.Context, exchange, key string, mandatory, immediate bool, msg amqp.Publishing) error умещает отправку в один вызов, и каждый аргумент здесь смысловой. Exchange — обменник, в который уходит сообщение; пустая строка означает дефолтный обменник, который маршрутизирует по имени очереди, поэтому первые примеры курса публикуют с exchange равным пустой строке и ключом tasks. Key — routing key: для дефолтного обменника это буквально имя очереди, для остальных — слово, по которому обменник сопоставляет привязки; разбор обменников по типам начинается в одиннадцатом уроке. Mandatory — просьба вернуть сообщение, если маршрута не нашлось; этому флагу ниже посвящён отдельный раздел. Immediate — исторический флаг: поддержка удалена из брокера ещё в релизе 3.0, true приводит к ошибке канала, поэтому в современном коде передают false. Msg — структура amqp.Publishing с телом и заголовками. Контекст ограничивает время ожидания отправки: если соединение заблокировано и кадр не уходит, вызов вернётся по дедлайну вместо вечного зависания. Возвращает вызов error — и почти во всех обстоятельствах nil, к чему вернёмся сразу после кода.
Продюсер: публикуем сообщение в очередь tasks
GOpackage main
import (
"context"
"log"
"time"
amqp "github.com/rabbitmq/amqp091-go"
)
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.QueueDeclare("tasks", true, false, false, false, nil)
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = ch.PublishWithContext(ctx, "", "tasks", false, false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte("order 42: send receipt"),
})
if err != nil {
log.Fatal(err)
}
log.Println("опубликовано")
}Продюсер объявляет очередь, как и потребитель, — идемпотентное объявление из пятого урока снимает гонку старта. Публикация идёт в дефолтный обменник с ключом tasks: сообщение попадает в очередь с тем же именем. Контекст с пятисекундным дедлайном страхует от зависания, если брокер перестал читать из сокета. Запустите программу несколько раз и откройте вкладку Queues в management UI: глубина очереди растёт с каждым запуском, пока нет потребителя.
Что вернёт брокер: nil не значит доставлено#
Самое частое заблуждение про rabbitmq publish: nil из PublishWithContext принимают за гарантию доставки. На самом деле nil означает, что сообщение покинуло библиотеку и ушло брокеру по сети. Дошло ли оно до очереди, легло ли на диск, забрал ли потребитель — из этого nil не следует ни первое, ни второе, ни третье. Маршрута нет — брокер по умолчанию молча выбрасывает сообщение, и это не ошибка: публикация вернёт nil. Брокер перезапустился вместе с недолговечной очередью — nil возвращён задолго до аварии, продюсер ни о чём не догадался. Для честных гарантий у протокола есть два отдельных механизма: mandatory, который возвращает сообщение, если маршрутизация не сработала, и publisher confirms — подтверждения брокера о приёме публикации, отдельный режим канала; к confirms курс вернётся, когда займётся устойчивостью к сбоям. Сейчас важно приучиться читать nil как «отправлено», а не «доставлено»: разница между этими словами на продакшене измеряется днями расследований.
amqp.Publishing: тело и заголовки сообщения#
Структура amqp.Publishing описывает сообщение целиком, и тело здесь лишь одно из полей. Body — это []byte: библиотека не сериализует ничего сама, строка, JSON или protobuf — ваша забота, а форматирование структур через JSON разберём в пятнадцатом уроке. ContentType подсказывает потребителю формат: text/plain для простых строк, application/json для структур — потребитель ветвится по нему, не заглядывая внутрь. Headers — карта amqp.Table для произвольных атрибутов: идемпотентные ключи, версия схемы, идентификатор трассировки из внешней системы. DeliveryMode определяет долговечность сообщения — единица против двойки, полный разбор в девятом уроке. Expiration задаёт срок жизни в миллисекундах строкой, Priority — приоритет для очередей с поддержкой приоритетов. Поля CorrelationId и ReplyTo пока просто запомните: на них держится RPC над очередями из восемнадцатого урока. Практическое правило: данные, по которым строят маршрутизацию и фильтрацию, выносят в заголовки и routing key, а тело держат компактным — большие файлы в очередь не кладут.
Поля amqp.Publishing на каждый день#
| Поле | Тип | Зачем нужно |
|---|---|---|
| Body | []byte | содержимое сообщения; сериализация на вашей стороне |
| ContentType | string | подсказка формата потребителю: text/plain, application/json |
| Headers | amqp.Table | произвольные атрибуты: трассировка, версия схемы, ключи дедупликации |
| DeliveryMode | uint8 | 1 — транзитное, 2 — persistent; подробности в девятом уроке |
| Expiration | string | TTL сообщения в миллисекундах, например 60000 |
| CorrelationId, ReplyTo | string | связка запроса и ответа в RPC-паттерне |
Кейс: очередь переименовали с tasks на task-jobs при рефакторинге, а продюсер одного из трёх сервисов в правку не попал. PublishWithContext с дефолтным обменником вернул nil, логи чистые, письма перестали уходить — сообщения улетали в обменник, не находили очередь и выбрасывались. Обнаружили по management UI: глубина новой очереди растёт, публикаций на старое имя в статистике нет. Диагноз ставится мгновенно, если публикация шла с mandatory=true и кто-то читал NotifyReturn: каждое недоставленное сообщение вернулось бы с текстом no-route. Дешевле включить это сразу, чем восстанавливать потерю по жалобам пользователей.
mandatory и NotifyReturn: возврат вместо молчания#
Флаг mandatory=true меняет поведение брокера для немаршрутизируемых сообщений: вместо молчаливого выброса он отправляет их обратно продюсеру методом basic.return. В amqp091-go возвраты приходят на канал, который регистрируется вызовом ch.NotifyReturn: метод возвращает chan amqp.Return, и читать его нужно в отдельной горутине — возврат асинхронный, к моменту возврата PublishWithContext уже вернул nil. Внутри amqp.Return лежит тело сообщения и поля протокола: ReplyText с причиной no-route, исходный routing key, имя обменника. Отсюда минимальная дисциплина продюсера: публиковать с mandatory=true в критичных местах и логировать каждый возврат как ошибку уровня alert. Отдельно про настоящие ошибки: публикация в несуществующий именованный обменник — это не возврат, а 404 NOT_FOUND, брокер закрывает канал, и ошибка всплывает в PublishWithContext обычным error. Дефолтный обменник с пустым именем существует у брокера по определению, поэтому до этого урока ошибки 404 у вас и не встречались.
NotifyReturn: ловим недоставленное
GOreturns := ch.NotifyReturn(make(chan amqp.Return, 16))
go func() {
for r := range returns {
log.Printf("возврат: %s, ключ %q, тело %s",
r.ReplyText(), r.RoutingKey(), r.Body)
}
}()
err := ch.PublishWithContext(ctx, "", "task-jobs", true, false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte("order 43"),
})
log.Println("publish err:", err)Очереди task-jobs нет, mandatory=true — и через мгновение горутина напечатает no-route с исходным ключом и телом. Публикация при этом вернёт nil: возврат идёт отдельным протокольным методом, когда ответ на publish уже не ждут. Буфер канала в шестнадцать смягчает всплеск возвратов; если буфер переполнен, уведомления молча отбрасываются — поэтому в боевом продюсере горутину держат читающей постоянно, а не заводят на один вызов.
Контекст, горутины и размер тела#
Публикация разделяет судьбу канала: PublishWithContext нельзя дёргать из нескольких горутин на одном канале — кадры перемешаются, и брокер закроет канал; правило одного канала на горутину из четвёртого урока действует и здесь. В HTTP-обработчике контекст удобно брать от запроса: клиент отвалился — уходит и отправка. Но есть нюанс, на котором спотыкаются: обрыв соединения пользователем — сомнительный повод терять задачу, поэтому для критичных публикаций я завожу отдельный контекст с дедлайном, независимым от запроса. Спорить можно о том, что правильнее для вашего трафика, но терять заказ из-за закрытой вкладки браузера мне не нравится. Второй нюанс — размер: сотня килобайт на тело обычно безболезненна, а десятки мегабайт превращают очередь в файловое хранилище и съедают пропускную способность; файл кладите в объектное хранилище, а в очередь — ссылку. Третий — дедупликация: при повторных отправках после сбоя потребитель получит копию, и бизнес-ключ в заголовках спасает от двойной обработки.
Публикация требует живого брокера: сети и сторонних пакетов в браузерной песочнице нет, там работают только fmt, strings, strconv, math, sort, errors и time. Поэтому продюсер этого урока запускается локально через go run против контейнера из второго урока, а интерактивы ниже проверяют сигнатуру, смысл nil и порядок вызовов — то, что чаще всего всплывает на код-ревью.
Правила публикации, которые экономят ночи#
- Объявляйте очередь и на стороне продюсера — идемпотентное объявление снимает гонку старта.
- Публикуйте через контекст с дедлайном: заблокированное соединение не должно подвешивать обработчик.
- Включайте mandatory=true там, где потеря недопустима, и читайте NotifyReturn в отдельной горутине.
- Читайте nil как «отправлено брокеру», а не «доставлено» — confirms и запись на диск отдельные темы.
- Один канал — одна горутина: публикация, как и весь канал, не потокобезопасна.
- Маршрутизационные данные — в routing key и заголовках, тело компактное, большие файлы — по ссылке.
Куда двигаться дальше: в седьмом уроке очередь наконец прочитают — ConsumeWithContext и цикл воркера, к которым эта публикация готовит сообщения. Если хочется освежить, как очередь обзавелась флагами, — пятый урок, а DeliveryMode и долговечность целиком разобраны в девятом. Здесь публикация шла через дефолтный обменник; настоящая маршрутизация начинается в одиннадцатом уроке про direct exchange. Для формата тел пригодится пятнадцатый урок про JSON, для запрос-ответа — восемнадцатый про RPC. Механика «положил и забрал» знакома вам по спискам Redis — правда, там никто не возвращает недоставленное. Карта всего курса — на странице курса.
Проверка: смысл nil
ch.PublishWithContext вернул nil. Что это значит?
Проверка: mandatory
Продюсер публикует с mandatory=true в обменник, для которого нет ни одной подходящей привязки. Что произойдёт?
Что выведет программа?
Прочитайте код и выберите, что он выведет:
ch.QueueDeclare("notifications", true, false, false, false, nil)
err := ch.PublishWithContext(ctx, "", "notification", false, false,
amqp.Publishing{
ContentType: "text/plain",
Body: []byte("hi"),
})
fmt.Println(err)Соберите публикацию
Допишите вызов публикации, структуру сообщения и поле с телом.
Соберите продюсера по строкам
Расставьте строки в порядке выполнения: соединение, проверки, закрытие, канал, контекст, публикация.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: PublishWithContext
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- PublishWithContext(ctx, exchange, key, mandatory, immediate, msg) — вся отправка сообщений rabbitmq в одном вызове канала.
- nil означает «отправлено брокеру»: маршрут, диск и получение потребителем из него не следуют.
- Без mandatory немаршрутизируемое сообщение выбрасывается молча; mandatory возвращает его через NotifyReturn.
- Публикация в несуществующий именованный обменник даёт 404 и закрытый канал; дефолтный обменник маршрутизирует по имени очереди.
- Body — []byte без сериализации; ContentType и Headers несут формат и атрибуты, DeliveryMode — долговечность.
- Контекст с дедлайном защищает от зависания на заблокированном соединении; канал — один на горутину.
- Флаг immediate мёртв с релиза 3.0 — в коде передают false.
Домашнее задание
Соберите продюсер из урока и опубликуйте три сообщения в очередь tasks, затем смените ключ публикации с tasks на task2 и убедитесь через management UI, что глубина очереди не растёт. После этого включите mandatory=true, добавьте NotifyReturn с логированием и покажите, что недоставленное сообщение вернулось с причиной no-route.
Личные заметкиПусто
Вопросы и ответы по уроку
Чем PublishWithContext отличается от устаревшего Publish?
Старый Publish не принимал контекст и оставлен в библиотеке для совместимости: заблокированная публикация висела без дедлайна. PublishWithContext принимает context.Context первым аргументом и позволяет ограничить ожидание отправки; современный код использует именно его.
Почему PublishWithContext вернул nil, а сообщения в очереди нет?
Самая вероятная причина — маршрут не нашёлся: опечатка в routing key или публикация в обменник без привязок. Без mandatory такое сообщение выбрасывается, публикация возвращает nil. Включите mandatory, читайте NotifyReturn, а глубину и статистику сверяйте в management UI.
Нужны ли publisher confirms, если уже включён mandatory?
Это разные механизмы: mandatory возвращает только немаршрутизируемые сообщения, а confirms — подтверждение брокера о приёме каждой публикации. Для сценария «брокер упал в момент приёма, а продюсер уверен, что отправил» нужны именно confirms — к ним курс вернётся в уроках про устойчивость к сбоям.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Потребление: ConsumeWithContext
- Ack, Nack и Reject: гарантии доставки
- Долговечность: durable и persistent
- Prefetch и fair dispatch: QoS потребителя
- Очередь: QueueDeclare и свойства
- Connection и Channel: модель соединений
- Подключение amqp091-go: первый клиент
- Установка RabbitMQ: Docker и management UI
- Все курсы по Go
Следующая ступень учебного пути
Excel в Go: чтение и генерация xlsx на excelize
Курс excelize на Go: создание xlsx, чтение, стили, формулы, диаграммы и StreamWriter для больших файлов. 25 уроков с примерами кода и проектом.
- Создавать и читать xlsx-файлы средствами excelize
- Работать с листами, ячейками, строками и столбцами
- Оформлять таблицы: стили, шрифты, границы, форматы чисел и дат
- Вставлять формулы, диаграммы и изображения