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

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

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

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

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

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

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

Учебного бота на двести пользователей заказчик просит «ещё уведомлять о новых статьях» — и вот тут простая на вид задача рассылки ломает наивный код. Цикл по списку 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 — сколько секунд ждать, — а при систематическом игнорировании начинает резать скорость надолго. Про обработку этих ответов подробно написано в уроке про ошибки и лимиты, здесь же правило: пауза дешевле бана скорости.

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

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 не различает ваши намерения.

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 разобрана в продвинутом курсе.

Что отвечает 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 мс. Сколько займёт рассылка?

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

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

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

Прочитайте код и выберите, что он выведет:

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)

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

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

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

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

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

Строки

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

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

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

Термин

Подписчик

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

chat_id пользователя, который написал боту /start. Хранится списком в памяти или множеством Redis, адресов помимо этого у бота нет.

1 / 6

Уведомления заработали в масштабе — теперь стоит заняться их качеством и расписанием: напоминания через тикер отправят персональные уведомления в заданное время, а разметка из урока про форматирование соберёт дайджест с жирными заголовками и ссылками. Хранилище подписчиков строится на карте с мьютексом и Redis из одиннадцатой части, а состояние диалога подскажет, как принять текст рассылки у администратора в два шага. Карта всех двадцати пяти уроков — на странице курса.

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

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • Рассылка — цикл по срезу 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 без задержки.

Личные заметкиПусто
Хранятся только в вашем браузере (Telegram · Рассылки: уведомления подписчикам)

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

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

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

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

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

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

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

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

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

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

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

Мониторинг Go с Prometheus: метрики, Grafana, алерты

Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.

  • Понимать четыре типа метрик и выбирать правильный
  • Экспортировать метрики из Go через promhttp
  • Инструментировать HTTP-запросы и бизнес-события
  • Писать PromQL-запросы: rate, sum, p99