Все пятнадцать предыдущих уроков бот жил на ноутбуке и работал в режиме long polling: цикл сам ходил в Telegram за обновлениями и ждал. Это удобно на старте, но у модели есть цена, которая становится заметной на сервере: постоянные исходящие запросы, задержка между событием и обработкой, невозможность масштабировать на два инстанса — апдейты уйдут в одного из поллингов, и никто не знает в какого. Webhook переворачивает стрелку: вы один раз говорите Telegram свой HTTPS-адрес командой setWebhook, и с этого момента каждое событие — сообщение, нажатие кнопки, inline-запрос — приезжает к вам POST-запросом с JSON-апдейтом в теле. Бот становится обычным HTTP-сервисом: порт, хендлер, nginx при желании, горизонтальное масштабирование за балансировщиком. Хорошая новость для тех, кто прошёл первые уроки: цикл обработки апдейтов не меняется вообще — меняется только источник, из которого цикл читает. В этом уроке разберём настройку на Go, подводный камень с включённым polling, серты и порты, отладку без Telegram и путь переезда на сервер.
Как устроен webhook#
Механика двухшаговая. Сначала бот сообщает Telegram адрес: конфигурация NewWebhook с HTTPS-URL передаётся методом Request — это отправка setWebhook на сервер. Дальше Telegram на каждое событие шлёт POST на этот адрес, и телом запроса лежит тот самый JSON-апдейт, который long polling получал в канале. Адрес обязан быть HTTPS: сервер отклонит http-URL ошибкой про то, что для вебхука требуется HTTPS. Сертификат подходит любой из двух: валидный от доверенного центра вроде Let's Encrypt или самоподписанный, который прикладывается к setWebhook через NewWebhookWithCert. Путь в URL — это ваш единственный барьер от подделок, и стандартная практика — использовать сам токен бота: Telegram знает полный адрес, посторонний не угадает, и каждый входящий запрос можно доверять по факту пути. Порт тоже ограничен: документация называет 443, 80, 88 и 8443 — слушать сервером что-то другое Telegram откажется.
Бот-сервер целиком
GOfunc main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("BOT_TOKEN"))
if err != nil {
log.Fatal(err)
}
wh := tgbotapi.NewWebhook("https://example.com/" + bot.Token)
if _, err := bot.Request(wh); err != nil {
log.Fatal(err)
}
updates := bot.ListenForWebhook("/" + bot.Token)
go http.ListenAndServe("0.0.0.0:8080", nil)
for update := range updates {
if update.Message != nil {
log.Println(update.Message.Text)
}
}
}Три сущности: конфигурация NewWebhook уходит методом Request — это и есть setWebhook, затем ListenForWebhook регистрирует приёмник по пути с токеном и возвращает канал, а HTTP-сервер поднимается в горутине. Main занят привычным циклом range по апдейтам — тот же код, что работал с long polling, читает новый источник.
ListenForWebhook под капотом#
Метод ListenForWebhook делает ровно две вещи и обе важны для понимания. Первая: он регистрирует HTTP-хендлер на переданном пути в стандартном роутере net/http — каждый POST на этот путь будет разобран. Вторая: в хендлере тело запроса декодируется из JSON в структуру tgbotapi.Update и отправляется в буферизованный канал, который метод возвращает вызывающему. Отсюда два практических правила. Первое — порядок строк: ListenForWebhook вызывается до старта HTTP-сервера, иначе сервер начнёт принимать запросы раньше, чем хендлер зарегистрирован, и первые апдейты упадут в 404. Второе — цикл range из уроков про polling работает без единой правки: источник сменился, интерфейс остался, и весь код обработки сообщений, callback и inline-запросов переезжает как есть. Буфер канала сглаживает всплески: Telegram шлёт апдейты параллельно и не ждёт, пока ваш цикл до них дойдёт, — при длительном заторе сервер начнёт возвращать ошибки, и Telegram будет повторять доставку.
Пока на боте установлен вебхук, запросы GetUpdates отклоняются ошибкой Conflict: terminated by other getUpdates request; make sure that only one bot instance is running. Типичный сценарий новичка: на сервере поднят webhook, а на ноутбуке ещё крутится старый polling-процесс — и оба выглядят сломанными: один получает Conflict, второй тише молчит. Telegram отдаёт апдейты только одному получателю. Лечение: оставить один способ получения, а перед возвратом к polling снять вебхук командой DeleteWebhook. Правило на память: один бот — один источник апдейтов, и второй процесс — это не резерв, а конкуренты за одни и те же сообщения.
HTTPS, порты и локальная отладка#
Главное неудобство webhook — проверка живьём требует публичного HTTPS-адреса, которого у локальной разработки нет. Рабочие варианты три. Первый — туннель: ngrok или ssh-туннель до сервера дают временный HTTPS-адрес, который на время отладки скармливается NewWebhook. Второй — curl: Telegram шлёт обычный POST с JSON, и тот же запрос можно сделать руками на localhost — хендлер получит апдейт и канал отработает, а Telegram для этого не нужен. Третий — сразу деплой на тестовый VPS с нормальным сертификатом, и здесь помогает связка с уроком про деплой. Токен в пути стоит держать сквозным: от Telegram до вашего хендлера, через nginx, если он есть, — тогда проверка подлинности остаётся в одном месте. Для продакшена конфигурация уточняется: AllowedUpdates срезает ненужные типы апдейтов ещё на стороне Telegram, а MaxConnections ограничивает параллельные соединения доставки.
Уточнённая настройка и сервер
GOwh := tgbotapi.WebhookConfig{
URL: "https://example.com/" + bot.Token,
AllowedUpdates: []string{"message", "callback_query"},
}
if _, err := bot.Request(wh); err != nil {
log.Fatal(err)
}
server := &http.Server{
Addr: "127.0.0.1:8080",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
updates := bot.ListenForWebhook("/" + bot.Token)
go server.ListenAndServe()Вместо конструктора здесь собрана структура WebhookConfig: AllowedUpdates говорит Telegram присылать только сообщения и нажатия, остальная почта отрезается до отправки. Сервер — не голый ListenAndServe, а http.Server с таймаутами на чтение и запись: webhook-доставка это короткие POST, и таймауты защищают от висящих соединений.
Кейс: переезд бота на VPS с nginx#
Типовой переезд выглядит так. На VPS ставится nginx, который завершает TLS на порту 443 с сертификатом Let's Encrypt, и проксирует запросы на внутренний адрес 127.0.0.1:8080, где живёт Go-сервер бота. NewWebhook получает внешний адрес вида https://bot.example.com/ с токеном в пути, nginx передаёт его как есть, и проверка подлинности остаётся в коде бота. Бот запускается юнитом systemd — настройкам автозапуска посвящён отдельный урок про деплой, — и после каждого рестарта setWebhook заново отправлять не нужно: Telegram хранит адрес до смены. Первые сутки после переезда стоит смотреть два лога: access-лог nginx на предмет посторонних запросов к пути с токеном — их не должно быть, а единичные можно молча отдавать 403 — и лог бота на предмет всплесков. Ежедневные апдейты приезжают параллельно, без длинных ожиданий, и латентность ответа бота падает с сотен миллисекунд polling до единиц миллисекунд доставки.
Возврат к polling
GO// снять вебхук и вернуться к long polling
if _, err := bot.Request(tgbotapi.DeleteWebhookConfig{}); err != nil {
log.Fatal(err)
}
u := tgbotapi.NewUpdate(0)
u.Timeout = 30
updates := bot.GetUpdatesChan(u)DeleteWebhookConfig снимает регистрацию адреса, и апдейты снова накапливаются на сервере Telegram до запроса GetUpdates. Дальше включается привычный канал из пятого урока: NewUpdate с офсетом и тайм-аутом, GetUpdatesChan и тот же цикл. Переключение между режимами стоит несколько строк, если код обработки вынесен отдельно от источника.
Когда polling всё ещё лучше#
Переехав на webhook, честно признать и обратную сторону. Пока бот живёт на ноутбуке, за NAT или в песочнице, у вас нет публичного HTTPS-адреса — и polling остаётся единственным способом получать апдейты без обвязки туннелями. Для отладки polling удобнее ещё и потому, что апдейты накапливаются на стороне Telegram: выключили процесс на ночь, включили утром — очередь пришла вся, а с вебхуком часть событий Telegram повторит, а часть отбросит. Небольшому боту на сотню пользователей вебхук ничего не даёт, кроме обязательного серта и лишнего компонента в схеме. Мой рабочий порог — переезд на сервер и аудитория в тысячи чатов: раньше этого polling не мешал никому, и половина продовых ботов спокойно живёт на нём годами. Переключение стоит пять строк, если источник апдейтов вынесен за обработку, так что решение не финальное и легко пересматривается в обе стороны.
Long polling против webhook#
| Параметр | Long polling | Webhook |
|---|---|---|
| Кто инициирует | Бот ходит за апдейтами | Telegram приносит POST |
| Адрес | Не нужен, работает из ноутбука | Публичный HTTPS с сертом |
| Порты | Любые | 443, 80, 88, 8443 |
| Отладка | Проще: ничего кроме токена | Туннель или curl руками |
| Масштабирование | Один поллер на бота | Несколько инстансов за балансировщиком |
| Когда выбирать | Разработка, первые проекты | Прод, аудитория, интеграции |
| Повтор доставки | Очередь копится на стороне Telegram | Повторы несколько часов, потом отброс |
Сохраните образец JSON-апдейта из debug-лога и подсовывайте его хендлеру через curl: curl -X POST localhost:8080/секретный-путь -d @update.json. Канал отработает как настоящий, и логику обработки можно гонять без Telegram вообще. Тот же приём ложится в юнит-тесты из урока про тестирование бота — HTTP-хендлер проверяется стандартными инструментами. Для ускорения эксперимента держите второй терминал с хвостом лога: каждый POST виден сразу, и опечатка в пути отлавливается за секунды.
Проверка: что делает ListenForWebhook
Что возвращает метод bot.ListenForWebhook("/" + token)?
Проверка: зачем токен в пути
Зачем в путь вебхука кладут токен бота?
Что ответит Telegram на http-адрес
Прочитайте код и выберите, что он выведет:
wh := tgbotapi.NewWebhook("http://example.com/hook")
_, err := bot.Request(wh)
fmt.Println(err)Соберите включение вебхука
Впишите конструктор, поле токена и методы регистрации.
Шесть строк перехода на webhook
Расставьте запуск бота-сервера: от создания клиента до цикла апдейтов.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: webhook
Бот стал HTTP-сервисом, и дальше маршрут раздваивается. Если остаётесь в режиме приёма, следующий шаг — фоновые задачи: урок про напоминания добавляет боту горутину с тикером, которая работает независимо от апдейтов. Обслуживание на сервере — сборка и systemd — разобраны в уроке про деплой. Откуда вебхук растёт: механика long polling и её ограничения остались в пятом уроке, а обвязка HTTP-серверов на Go с роутерами и middleware — тема курса по chi. Полный маршрут курса — на его странице.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Webhook переворачивает направление: не бот ходит за апдейтами, а Telegram приносит их POST-запросами на HTTPS-адрес.
- setWebhook отправляется конфигурацией NewWebhook через bot.Request; ListenForWebhook регистрирует приёмник и возвращает канал.
- Цикл range по апдейтам не меняется: источник сменился, код обработки переезжает как есть.
- Путь с токеном — секрет доставки: посторонний POST не угадает адрес, а порты ограничены 443, 80, 88 и 8443.
- Polling и webhook не работают одновременно: GetUpdates отвечает 409 Conflict, возврат — через DeleteWebhook.
- HTTPS обязателен: валидный сертификат, самоподписанный через NewWebhookWithCert или туннель для отладки.
- AllowedUpdates и http.Server с таймаутами — минимальная прод-настройка приёма.
- Отладка без Telegram: сохранённый JSON-апдейт уходит в хендлер через curl, и канал отрабатывает как настоящий.
Домашнее задание
Переведите локального бота на webhook с туннелем или проверьте хендлер через curl с сохранённым JSON-апдейтом: NewWebhook с адресом и токеном в пути, Request на setWebhook, ListenForWebhook до старта сервера, http.Server с таймаутами в горутине. Убедитесь, что цикл range обрабатывает апдейты без правок, затем попробуйте запустить рядом polling-процесс и поймайте в логе ошибку 409 Conflict — теперь вы знаете её в лицо. Завершите упражнение возвратом: DeleteWebhookConfig и polling из пятого урока снова работает.
Личные заметкиПусто
Вопросы и ответы по уроку
Нужен ли вебхук, если бот маленький и живёт на ноутбуке?
Нет: для разработки и первых проектов long polling проще — не нужен ни адрес, ни сертификат, ни открытый порт. Вебхук окупается на сервере: прод-аудитория, интеграции, желание масштабироваться и измерять латентность доставки. Переключение туда-обратно стоит несколько строк, если источник апдейтов вынесен за обработку.
Можно ли слушать произвольный порт, например 3000?
Telegram принимает доставку только на 443, 80, 88 и 8443. Ваше приложение может слушать любой порт внутри — например, 8080 за nginx, — но внешний вход для Telegram должен заканчиваться на одном из разрешённых портов.
Что происходит с апдейтами, если сервер был недоступен?
Telegram несколько часов повторяет неудавшуюся доставку с отступающими интервалами, потом отбрасывает апдейт. Порядок при этом сохраняется по чату, но не гарантируется глобально. Для критичных к потере задач бот обязан дублировать состояние у себя — очередь в Redis или журнал событий — и не полагаться только на доставку Telegram.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Напоминания: goroutine и тикер
- Платежи: инвойсы и Telegram Stars
- Mini Apps: веб-интерфейс внутри Telegram
- Реакции, стикеры и живой тон бота
- Ошибки и лимиты Telegram: 429 и RetryAfter
- Редактирование сообщений и многостраничное меню
- Inline-режим: бот в поле поиска
- Рассылки: уведомления подписчикам
- Все курсы по Go
Следующая ступень учебного пути
Мониторинг Go с Prometheus: метрики, Grafana, алерты
Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.
- Понимать четыре типа метрик и выбирать правильный
- Экспортировать метрики из Go через promhttp
- Инструментировать HTTP-запросы и бизнес-события
- Писать PromQL-запросы: rate, sum, p99