Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 6 из 25 ~18 мин +20 XP

Публикация: PublishWithContext

Отправка сообщений в RabbitMQ из Go выглядит обманчиво просто: один вызов rabbitmq publish go через PublishWithContext — и сообщение ушло. Под этим вызовом прячутся вопросы, на которых теряют данные: что именно значит nil в ответе, куда девается сообщение, для которого не нашлось очереди, и что положить в тело и заголовки. Разберём сигнатуру по частям и соберём продюсер, который не молчит о потерях.

Содержание урока · 10 разделов

Очередь из пятого урока объявлена и ждёт. Кажется, осталось заменить чтение на запись — и конвейер работает. Но публикация — самое обманчивое место брокера: продюсер получает 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

GO
package 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содержимое сообщения; сериализация на вашей стороне
ContentTypestringподсказка формата потребителю: text/plain, application/json
Headersamqp.Tableпроизвольные атрибуты: трассировка, версия схемы, ключи дедупликации
DeliveryModeuint81 — транзитное, 2 — persistent; подробности в девятом уроке
ExpirationstringTTL сообщения в миллисекундах, например 60000
CorrelationId, ReplyTostringсвязка запроса и ответа в RPC-паттерне

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: ловим недоставленное

GO
returns := 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-обработчике контекст удобно брать от запроса: клиент отвалился — уходит и отправка. Но есть нюанс, на котором спотыкаются: обрыв соединения пользователем — сомнительный повод терять задачу, поэтому для критичных публикаций я завожу отдельный контекст с дедлайном, независимым от запроса. Спорить можно о том, что правильнее для вашего трафика, но терять заказ из-за закрытой вкладки браузера мне не нравится. Второй нюанс — размер: сотня килобайт на тело обычно безболезненна, а десятки мегабайт превращают очередь в файловое хранилище и съедают пропускную способность; файл кладите в объектное хранилище, а в очередь — ссылку. Третий — дедупликация: при повторных отправках после сбоя потребитель получит копию, и бизнес-ключ в заголовках спасает от двойной обработки.

Правила публикации, которые экономят ночи#

  • Объявляйте очередь и на стороне продюсера — идемпотентное объявление снимает гонку старта.
  • Публикуйте через контекст с дедлайном: заблокированное соединение не должно подвешивать обработчик.
  • Включайте 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)

Соберите публикацию

Допишите вызов публикации, структуру сообщения и поле с телом.

err := ch.(ctx, "", "tasks", false, false, amqp.{ ContentType: "text/plain", : []byte("order 42"), })

Соберите продюсера по строкам

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

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: PublishWithContext

Термин

PublishWithContext

нажмите, чтобы перевернуть
Определение

Публикация сообщения на канале: контекст, обменник, routing key, mandatory и immediate, структура amqp.Publishing. Возвращает error — в большинстве случаев nil.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +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.

Личные заметкиПусто
Хранятся только в вашем браузере (RabbitMQ · Публикация: PublishWithContext)

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

Чем PublishWithContext отличается от устаревшего Publish?

Старый Publish не принимал контекст и оставлен в библиотеке для совместимости: заблокированная публикация висела без дедлайна. PublishWithContext принимает context.Context первым аргументом и позволяет ограничить ожидание отправки; современный код использует именно его.

Почему PublishWithContext вернул nil, а сообщения в очереди нет?

Самая вероятная причина — маршрут не нашёлся: опечатка в routing key или публикация в обменник без привязок. Без mandatory такое сообщение выбрасывается, публикация возвращает nil. Включите mandatory, читайте NotifyReturn, а глубину и статистику сверяйте в management UI.

Нужны ли publisher confirms, если уже включён mandatory?

Это разные механизмы: mandatory возвращает только немаршрутизируемые сообщения, а confirms — подтверждение брокера о приёме каждой публикации. Для сценария «брокер упал в момент приёма, а продюсер уверен, что отправил» нужны именно confirms — к ним курс вернётся в уроках про устойчивость к сбоям.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Excelize25 уроков~10 чпрактика25 уроков уже доступны

Excel в Go: чтение и генерация xlsx на excelize

Курс excelize на Go: создание xlsx, чтение, стили, формулы, диаграммы и StreamWriter для больших файлов. 25 уроков с примерами кода и проектом.

  • Создавать и читать xlsx-файлы средствами excelize
  • Работать с листами, ячейками, строками и столбцами
  • Оформлять таблицы: стили, шрифты, границы, форматы чисел и дат
  • Вставлять формулы, диаграммы и изображения