---
title: "Рассылки: уведомления подписчикам"
description: "Рассылка в telegram-боте на Go: список chat_id подписчиков, time.Sleep 80 мс между отправками, ошибки 403 Forbidden и 429 Too Many Requests. Пример кода."
course: "Telegram-боты на Go: от эха до полноценного сервиса"
url: "https://golangmaster.ru/courses/telegram-bot/lessons/14-rassylki/"
published: "2026-09-19"
updated: "2026-09-25"
minutes: 18
---

# Рассылки: уведомления подписчикам

*Курс «Telegram-боты на Go: от эха до полноценного сервиса», урок 14 из 25. ~18 минут. Обновлено 25.09.2026. Источник: https://golangmaster.ru/courses/telegram-bot/lessons/14-rassylki/*

Рассылка telegram-бота — это срез chat_id, цикл и пауза между отправками: без паузы Telegram отвечает 429 Too Many Requests, а с ней уведомления дотекают до всех подписчиков за минуты. Здесь собираем подписку через /start, пишем функцию рассылки с time.Sleep на Go, чистим список от заблокировавших бот и отправляем всё в фоне, не мешая циклу обновлений.

Учебного бота на двести пользователей заказчик просит «ещё уведомлять о новых статьях» — и вот тут простая на вид задача рассылки ломает наивный код. Цикл по списку chatID с bot.Send внутри выглядит рабочим: первые десятки сообщений уходят, потом Telegram начинает отвечать 429 Too Many Requests, а при особенно рьяной скорости — надолго резать лимиты всему боту. Второй сюрприз ждёт позже: часть адресов отвечает 403 Forbidden, потому что люди заблокировали бота за год, а список никто не чистил. Рассылка — первая функция бота, где ошибки API становятся нормой, а не исключением: она долбит в сотни чатов подряд, и любой нюанс лимитов сразу виден на цифрах. Хорошая новость в том, что все решения урока — пауза, чистка, фоновый запуск — умещаются в три десятка строк и переносятся между ботами без изменений. В этом уроке соберём подписку, напишем вежливую функцию рассылки с паузой, разберём обе ошибки и вынесем отправку в горутину.

### Подписчики: кто получит рассылку

Начало всему — список адресатов, а адресат в Bot API один: chat_id. Чат с ботом начинается командой /start — это и есть точка подписки: обработчик добавляет From.ID или Chat.ID в хранилище подписчиков. Отписка — команда /stop или слово «стоп», и её наличие обязательно: рассылка без пути назад выжигает доверие и собирает блокировки, о которых ниже. Хранится список тем же способом, что и данные анкеты из урока про хранение: в памяти — map[int64]bool под мьютексом, на подольше — Redis, где подписчики лежат множеством SAdd и удалить участника можно без чтения всего списка. Нюанс, о котором забывают: bot.GetChat и поиск по username в рассылке не участвуют — нельзя «найти всех пользователей Telegram», только тех, кто сам написал боту. Поэтому список подписчиков — актив бота, который стоит беречь: вежливость, частота и полезность рассылок определяют, сколько адресов переживёт месяц.

### Подписка и отписка

```go
type Subscribers struct {
        mu    sync.RWMutex
        chats map[int64]bool
}

func (s *Subscribers) Add(chatID int64) {
        s.mu.Lock()
        s.chats[chatID] = true
        s.mu.Unlock()
}

func (s *Subscribers) Remove(chatID int64) {
        s.mu.Lock()
        delete(s.chats, chatID)
        s.mu.Unlock()
}

func (s *Subscribers) List() []int64 {
        s.mu.RLock()
        defer s.mu.RUnlock()
        ids := make([]int64, 0, len(s.chats))
        for id := range s.chats {
                ids = append(ids, id)
        }
        return ids
}
```

Тот же протокол мьютекса, что и у хранилища анкеты: замок на каждую операцию, наружу — только методы. List копирует ключи в срез: рассылка работает по копии, и одновременная отписка не рвёт цикл посреди прохода. map[int64]bool вместо среза берётся ради дешёвых Add и Remove без поиска по списку.

### Пауза между отправками

Лимиты Telegram на отправку не опубликованы как константы, но в официальном FAQ ботов назван ориентир: примерно тридцать сообщений в секунду на одного бота в разные чаты, а в один чат — не чаще одного сообщения в секунду. Практическая производная: пауза 50-100 миллисекунд между Send держит бота под порогом с запасом. В коде это time.Sleep(80 * time.Millisecond) в конце каждой итерации цикла рассылки — самой обычной, без хитростей. Двести подписчиков с паузой 80 мс — шестнадцать секунд на всю рассылку, и это нормально: уведомления не горят. Соблазн ускорить есть всегда, и расплата предсказуема: Telegram сначала шлёт 429 с полем RetryAfter — сколько секунд ждать, — а при систематическом игнорировании начинает резать скорость надолго. Про обработку этих ответов подробно написано в [уроке про ошибки и лимиты](/courses/telegram-bot/lessons/17-oshibki-i-limity/), здесь же правило: пауза дешевле бана скорости.

### Функция рассылки

```go
func broadcast(bot *tgbotapi.BotAPI, subs *Subscribers, text string) {
        for _, id := range subs.List() {
                msg := tgbotapi.NewMessage(id, text)
                _, err := bot.Send(msg)
                if err != nil {
                        log.Println("mail:", id, err)
                }
                time.Sleep(80 * time.Millisecond)
        }
}
```

Цикл идёт по копии списка из List, на каждого подписчика — NewMessage с его chat_id и общей строкой текста. Ошибка отправки не прерывает рассылку: один недоступный чат не отменяет сообщения остальным. Пауза стоит после каждой отправки, включая неудачные, — счётчик скорости у Telegram не различает ваши намерения.

> **429: Telegram просит сбавить скорость.** Ответ API с кодом 429 Too Many Requests приходит с полем retry_after — числом секунд, на которые стоит заткнуться. Игнорировать нельзя: после пары проигнорированных 429 лимиты режутся жёстче и дольше. В библиотеке ошибка приходит как *tgbotapi.Error с полем RetryAfter — пауза в цикле рассылки выставляется по нему, а не по константе.

### 403 Forbidden: список с мёртвыми душами

Вторая ошибка рассылки накопительная: пользователь заблокировал бота, и каждый новый выпуск уведомлений упирается в 403 Forbidden по его адресу. Сама ошибка безобидна — сообщение не уйдёт, остальные дойдут, — но без чистки список растёт, а эффективная скорость падает: пауза-то тратится на каждого адресата, включая мёртвых. Лечение — удалять подписчика при Forbidden: ошибка отправки разбирается строкой, и встреча с Forbidden вызывает Remove. Отдельно различаются два ответа: Forbidden — пользователь вас заблокировал, chat not found — бота удалили или чат не существует; для чистки списка они равнозначны, оба означают мёртвый адрес. Восстановление возможно только одним путём: пользователь вернётся и напишет /start — точка подписки та же, что и в начале. Держать мёртвые адреса в надежде на возврат можно, но тогда честно считать их отдельно, чтобы статистика доставки не врала. Чистка при отправке стоит одну ветку if и работает без вашего участия — редкое вложение с такой отдачей, аналогичное экранированию ввода из урока про форматирование.

### Чистка при отправке

```go
_, err := bot.Send(msg)
if err != nil {
        if strings.Contains(err.Error(), "Forbidden") ||
                strings.Contains(err.Error(), "chat not found") {
                log.Println("убираю:", id)
                subs.Remove(id)
        } else {
                log.Println("ошибка:", id, err)
        }
}
```

Ошибка API приходит текстом, и Contains здесь честный способ распознать причину: Forbidden и chat not found означают мёртвый адрес, остальное — временные беды вроде сети, которые не повод удалять живого подписчика. Remove защищён мьютексом внутри Subscribers, параллельная горутина напоминаний не сломается.

### Вариация: рассылка в фоне

Шестнадцать секунд рассылки внутри цикла обновлений — шестнадцать секунд бот не отвечает никому: GetUpdatesChan ждёт, пока обработчик освободится, а пользователи видят тишину. Решение из стандартного набора Go — отправить рассылку в горутину: go broadcast(bot, subs, text), и цикл обновлений продолжает работать. Здесь приходит время дисциплины из урока про горутины: во-первых, рассылка должна быть единовременной — флаг или семафор, иначе две команды /send подряд запустят два цикла с половинной скоростью каждый; во-вторых, goroutine не должна делить мутируемое состояние без замка — Subscribers уже защищён, и это не случайность, а условие жизни фонового кода. Для редких уведомлений хватает и goroutine на вызов, для регулярных — тикер из урока про напоминания, который раз в день сам запускает вежливую отправку. Механика goroutine с WaitGroup разобрана в [продвинутом курсе](/courses/go-advanced/lessons/04-gorutiny/).

> **Тест на десяти адресах.** Полную скорость рассылки не проверяют на живых подписчиках: заведите десяток тестовых чатов — свой chat_id, второй телефон, друзья, — и гоняйте паузы на них. Локальный Go, реальный токен, десять адресатов и лог с временем отправки покажут и паузу, и 403, и 429 — быстрее любого чтения документации.

### Что отвечает Telegram при рассылке

| Ответ | Что значит | Что делать боту |
| --- | --- | --- |
| 200 OK | сообщение доставлено | спать 80 мс |
| 403 Forbidden | пользователь заблокировал бота | убрать из списка |
| 400 chat not found | чат не существует | убрать из списка |
| 429 + retry_after | превышена скорость | спать по retry_after |
| таймаут сети | временный сбой | лог и продолжить |

### Кейс: дайджест по пятницам

Бот студенческого клуба копит новости в течение недели: команда /news от модератора кладёт строку в Redis-список, и в пятницу в 18:00 тикер из урока про напоминания просыпается, собирает все накопленные строки и запускает broadcast. Дайджест решает главную проблему частых рассылок — шум: пять уведомлений в день выжигают подписчиков, одно в неделю с содержанием — держит. Собранные строки склеиваются с нумерацией, сверху — заголовок с датой, и весь выпуск уходит одной рассылкой с той же паузой 80 мс. Если за неделю ничего не накопилось, честнее пропустить выпуск, чем слать «новостей нет»: пустое уведомление по статистике каналов собирает блокировки ничуть не хуже рекламы. Схема копилка-плюс-выпуск — заготовка и для напоминаний, и для отчётов, и она же объясняет, почему пауза в broadcast важнее скорости: выпусков мало, а репутация адресата одна.

### Спорное мнение: рассылка — не маркетинговый канал

Заказчики любят формулировать рассылку как «канал продвижения», и я с этим борюсь на этапе ТЗ. Бот, которому доверили chat_id, получил не рекламную базу, а канал уведомлений: это ответ на явное действие — подписку — и содержание должно соответствовать обещанию при /start. Граница, где я сдаюсь: подборка по явному интересу — пользователь попросил «присылай дайджест по Go» — это уведомление, даже если в нём есть ссылка на сайт. А вот «привет, у нас акция» списку молчаливых адресатов — путь к кнопке «заблокировать», и за ней 403, который не лечится. Практический критерий из продуктовых практик: если сообщение отвечает на вопрос «что нового по тому, о чём я просил», оно в формате уведомления; если на вопрос «что ещё продать», это спам, и Telegram с числом блокировок согласится с вами раньше, чем юрист.

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

**Вопрос:** В списке 250 подписчиков, пауза 80 мс. Сколько займёт рассылка?

- Около 8 секунд
- ✅ Около 20 секунд
- Около 2 минут
- Зависит от скорости сервера бота

250 отправок по 80 мс — двадцать секунд чистой паузы плюс время ответов API. Рассылка измеряется десятками секунд и это нормально: попытка ужать интервал упирается в 429 и в итоге затягивается сильнее.

### Проверка: мёртвый адрес

**Вопрос:** Send вернул 403 Forbidden для одного подписчика из списка. Что делает правильный обработчик?

- Прерывает всю рассылку
- Пауза 10 минут и повтор
- ✅ Удаляет адрес из списка и продолжает
- Отправляет сообщение ещё раз

Forbidden означает блокировку бота пользователем: адрес мёртв, повтор не поможет, а прерывание накажет всех остальных. Правильная реакция — Remove и следующая итерация цикла.

### Сколько сообщений уйдёт

```go
blocked := map[int64]bool{200: true, 300: true}
ids := []int64{100, 200, 300, 400}
sent := 0

for _, id := range ids {
        if blocked[id] {
                continue
        }
        sent++
}

fmt.Println(sent)
```

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

- 1
- ✅ 2
- 3
- 4

continue пропускает заблокированных без счётчика — останутся 100 и 400, sent равен 2. Точно так же работает чистка списка при Forbidden: мёртвые адреса не получают паузу и не считаются доставленными.

### Соберите цикл рассылки

Впишите источник адресов, признак блокировки и паузу.

```go
for _, id := range ___ {
        msg := tgbotapi.NewMessage(id, text)
        _, err := bot.Send(msg)
        if strings.Contains(err.Error(), "___") {
                subs.Remove(id)
        }
        ___(80 * time.Millisecond)
}
```

Подсказка: Адреса берутся из subs.List(), блокировка приходит как Forbidden, а пауза — time.Sleep из пакета time.

Правильные ответы в пропусках: `subs.List()`, `Forbidden`, `time.Sleep`

### Шесть строк тела рассылки

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

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

1. `msg := tgbotapi.NewMessage(id, text)`
2. `_, err := bot.Send(msg)`
3. `if err != nil {`
4. `  log.Println("mail:", id, err)`
5. `}`
6. `time.Sleep(80 * time.Millisecond)`

### Карточки: рассылки и уведомления

- **Подписчик** — chat_id пользователя, который написал боту /start. Хранится списком в памяти или множеством Redis, адресов помимо этого у бота нет.
- **Пауза рассылки** — time.Sleep 50-100 мс после каждой отправки: ориентир Telegram — около 30 сообщений в секунду, в один чат — не чаще раза в секунду.
- **403 Forbidden** — Пользователь заблокировал бота. Адрес мёртв: удаляется из списка, вернуть можно только новым /start от человека.
- **429 и retry_after** — Превышена скорость отправки. В ответе приходит число секунд ожидания; игнорирование ужесточает лимиты.
- **Фоновая рассылка** — go broadcast запускает отправку в горутине, цикл обновлений продолжает отвечать. Нужны защита состояния и запрет параллельных запусков.
- **Отписка** — Команда /stop вызывает Remove: путь выхода обязателен, иначе список собирает блокировки и теряет доставляемость.

Уведомления заработали в масштабе — теперь стоит заняться их качеством и расписанием: [напоминания через тикер](/courses/telegram-bot/lessons/19-napominaniya/) отправят персональные уведомления в заданное время, а разметка из [урока про форматирование](/courses/telegram-bot/lessons/13-formatirovanie/) соберёт дайджест с жирными заголовками и ссылками. Хранилище подписчиков строится на [карте с мьютексом и Redis из одиннадцатой части](/courses/telegram-bot/lessons/11-hranenie-dannyh/), а [состояние диалога](/courses/telegram-bot/lessons/10-sostoyanie-dialoga/) подскажет, как принять текст рассылки у администратора в два шага. Карта всех двадцати пяти уроков — на [странице курса](/courses/telegram-bot/).

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

- Рассылка — цикл по срезу chat_id с bot.Send и паузой 50-100 мс между отправками.
- Подписка собирается на /start, отписка на /stop; список живёт в памяти под мьютексом или в Redis множеством.
- Ориентир лимитов Telegram — около 30 сообщений в секунду; 429 с retry_after требует паузы по числу секунд.
- 403 Forbidden и chat not found означают мёртвый адрес: подписчик удаляется, рассылка продолжается.
- List отдаёт копию списка — отписка во время рассылки не рвёт цикл.
- Долгую рассылку отправляют в горутине, чтобы цикл обновлений продолжал отвечать; параллельные запуски запрещают флагом.
- Уведомление — ответ на явную подписку; реклама молчаливому списку кончается кнопкой блокировки и 403.

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

Добавьте локальному боту подписку: /start записывает chat_id в Subscribers, /stop удаляет, команда /announce у админа принимает текст и запускает рассылку в горутине с паузой 80 мс. Проверьте сценарии: рассылка на трёх адресах приходит всем, заблокировавший бот адрес выпадает из списка после Forbidden, и во время рассылки бот отвечает на /help без задержки.

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

### Можно ли слать сообщения пользователям, которые не писали боту?

Нет: бот общается только с чатами, где уже есть диалог. Это фундаментальное правило Bot API — отсюда и ценность подписчиков: каждый адрес в списке значит, что человек сам нажал Start.

### Как узнать точные лимиты отправки для своего бота?

Точных опубликованных чисел нет, в FAQ назван ориентир около 30 сообщений в секунду на бота. Лимиты динамические и зависят от истории бота, поэтому честный способ — двигаться с паузой и слушать 429 с retry_after.

### Куда деть текст рассылки, если он приходит сообщением от админа?

Собирать в состояние диалога из урока про FSM: команда /announce переводит админа на шаг текста, следующее сообщение сохраняется и уходит в broadcast. Так текст не ломается пробелами в аргументах команды.
