---
title: "Публикация: PublishWithContext"
description: "PublishWithContext в Go: тело и заголовки amqp.Publishing, mandatory и NotifyReturn, почему nil не значит доставлено. Отправка сообщений rabbitmq с примерами."
course: "RabbitMQ в Go: очереди сообщений на amqp091-go"
url: "https://golangmaster.ru/courses/rabbitmq/lessons/06-publikaciya-soobschenij/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

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

*Курс «RabbitMQ в Go: очереди сообщений на amqp091-go», урок 6 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/rabbitmq/lessons/06-publikaciya-soobschenij/*

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

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

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

> **Код урока — локально, интерактивы — на странице.** Публикация требует живого брокера: сети и сторонних пакетов в браузерной песочнице нет, там работают только fmt, strings, strconv, math, sort, errors и time. Поэтому продюсер этого урока запускается локально через go run против контейнера из второго урока, а интерактивы ниже проверяют сигнатуру, смысл nil и порядок вызовов — то, что чаще всего всплывает на код-ревью.

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

- Объявляйте очередь и на стороне продюсера — идемпотентное объявление снимает гонку старта.
- Публикуйте через контекст с дедлайном: заблокированное соединение не должно подвешивать обработчик.
- Включайте mandatory=true там, где потеря недопустима, и читайте NotifyReturn в отдельной горутине.
- Читайте nil как «отправлено брокеру», а не «доставлено» — confirms и запись на диск отдельные темы.
- Один канал — одна горутина: публикация, как и весь канал, не потокобезопасна.
- Маршрутизационные данные — в routing key и заголовках, тело компактное, большие файлы — по ссылке.

Куда двигаться дальше: в [седьмом уроке](/courses/rabbitmq/lessons/07-potreblenie-soobschenij/) очередь наконец прочитают — ConsumeWithContext и цикл воркера, к которым эта публикация готовит сообщения. Если хочется освежить, как очередь обзавелась флагами, — [пятый урок](/courses/rabbitmq/lessons/05-ochered-prostaya/), а DeliveryMode и долговечность целиком разобраны в [девятом](/courses/rabbitmq/lessons/09-durable-persistent/). Здесь публикация шла через дефолтный обменник; настоящая маршрутизация начинается в [одиннадцатом уроке про direct exchange](/courses/rabbitmq/lessons/11-obmenniki-direct/). Для формата тел пригодится [пятнадцатый урок про JSON](/courses/rabbitmq/lessons/15-json-serializaciya/), для запрос-ответа — [восемнадцатый про RPC](/courses/rabbitmq/lessons/18-rpc-pattern/). Механика «положил и забрал» знакома вам по [спискам Redis](/courses/go-redis/lessons/11-spiski/) — правда, там никто не возвращает недоставленное. Карта всего курса — на [странице курса](/courses/rabbitmq/).

### Проверка: смысл nil

**Вопрос:** ch.PublishWithContext вернул nil. Что это значит?

- ✅ Сообщение отправлено брокеру — о гарантиях доставки nil не говорит
- Сообщение записано на диск и доставлено потребителю
- Сообщение попало в очередь и ждёт потребителя
- Соединение закрыто, сообщение потеряно

nil значит, что публикация покинула библиотеку и ушла брокеру. Маршрут, запись на диск и получение потребителем — отдельные механизмы: mandatory с NotifyReturn, publisher confirms и ручные ack соответственно.

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

**Вопрос:** Продюсер публикует с mandatory=true в обменник, для которого нет ни одной подходящей привязки. Что произойдёт?

- PublishWithContext вернёт ошибку маршрутизации
- ✅ Сообщение вернётся через NotifyReturn, публикация вернёт nil
- Сообщение останется в обменнике и дождётся привязки
- Брокер закроет канал ошибкой 404

Мandatory превращает молчаливый выброс в возврат: сообщение приходит в канал NotifyReturn с причиной no-route, когда nil уже возвращён. Ошибка 404 случается при публикации в несуществующий именованный обменник, а не при отсутствии маршрута.

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

```go
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)
```

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

- ✅ <nil> — сообщение выброшено: маршрута нет
- ошибка: очередь notifications не существует
- <nil> — сообщение лежит в очереди notifications
- канал закрыт, ошибка NOT_FOUND

Ключ notification не совпадает с именем очереди notifications, дефолтный обменник не нашёл маршрут — без mandatory сообщение выбрасывается, публикация возвращает nil. Одна буква s в опечатке, чистый лог и потерянные сообщения: классика продакшена.

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

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

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

Подсказка: Метод канала — PublishWithContext, структура сообщения — amqp.Publishing, содержимое лежит в поле Body.

Правильные ответы в пропусках: `PublishWithContext`, `Publishing`, `Body`

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

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

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

1. `conn, err := amqp.Dial(url)`
2. `if err != nil { log.Fatal(err) }`
3. `defer conn.Close()`
4. `ch, err := conn.Channel()`
5. `ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)`
6. `err = ch.PublishWithContext(ctx, "", "tasks", false, false, msg)`

### Карточки: PublishWithContext

- **PublishWithContext** — Публикация сообщения на канале: контекст, обменник, routing key, mandatory и immediate, структура amqp.Publishing. Возвращает error — в большинстве случаев nil.
- **amqp.Publishing** — Структура сообщения: Body []byte, ContentType, Headers amqp.Table, DeliveryMode, Expiration, Priority, CorrelationId и ReplyTo. Сериализацией библиотека не занимается.
- **routing key** — Ключ маршрутизации: для дефолтного обменника — имя очереди, для остальных — слово, по которому обменник выбирает привязки.
- **mandatory** — Флаг публикации: если маршрута нет, брокер возвращает сообщение методом basic.return вместо молчаливого выброса. Возвраты читают из NotifyReturn.
- **NotifyReturn** — Канал возвратов недоставленных сообщений: amqp.Return с телом, причиной no-route и исходным ключом. Возвраты асинхронны, читают в отдельной горутине.
- **immediate** — Устаревший флаг публикации: поддержка удалена из RabbitMQ с релиза 3.0, true приводит к ошибке канала. В коде передают false.

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

- 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 — к ним курс вернётся в уроках про устойчивость к сбоям.
