Содержание урока · 10 разделов
- Первый телеграм бот: полный код эхо-бота
- Как проверять бота вживую
- Кейс: бот-справка для студенческого чата
- ReplyToMessageID: почему бот цитирует вас
- Чего в эхо-боте сознательно нет
- Эхо в личке и в группе: разное поведение
- Типичная ошибка: медленная обработка внутри цикла
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Эхо-бот — hello world этой области: что человек написал, то бот и вернул, да ещё с цитированием. Схема тривиальная, но проходит через весь конвейер настоящего бота: получить обновление, проверить, что в нём сообщение, достать текст и chat_id, собрать ответ, отправить, обработать ошибку отправки. Каждый элемент этой цепочки в следующих уроках вырастет в отдельную тему, поэтому эхо-бот полезнее, чем кажется: это работающий каркас, к которому дальше прикручиваются команды, клавиатуры и состояния диалога. Именно поэтому первый телеграм бот стоит писать руками, а не копировать: пальцы запоминают порядок проверок лучше глаз. Приготовьте терминал: токен лежит в переменной окружения, модуль из третьего урока собран, библиотека установлена. Запускать будем локально — песочница сайта без сети, а боту она нужна постоянно.
Первый телеграм бот: полный код эхо-бота#
Вот весь эхо-бот. Не пугайтесь объёма: треть здесь — импорты и обработка ошибок, которая в учебных примерах часто выброшена, а в рабочем коде обязательна. Скопируйте в main.go, подставьте переменную окружения с токеном и запустите, затем напишите боту что-нибудь из Telegram — и он ответит цитатой вашего сообщения. Разбор каждой строки идёт сразу после кода, а после него — две правки, которые превратят безликое эхо в живого собеседника.
main.go эхо-бота целиком
GOpackage main
import (
"log"
"os"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
func main() {
bot, err := tgbotapi.NewBotAPI(os.Getenv("BOT_TOKEN"))
if err != nil {
log.Panic(err)
}
log.Printf("Авторизован под аккаунтом %s", bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
continue
}
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
msg.ReplyToMessageID = update.Message.MessageID
if _, err := bot.Send(msg); err != nil {
log.Println("ошибка отправки:", err)
}
}
}NewBotAPI с токеном из окружения — уже знакомая проверка getMe. NewUpdate(0) и Timeout = 60 описывают опрос, GetUpdatesChan возвращает канал обновлений, а цикл for range живёт, пока процесс не убьют. Внутри: проверка Message на nil, сборка ответа NewMessage из chat_id и текста входящего, реплай через ReplyToMessageID и отправка bot.Send с логированием ошибки. Тридцать строк — и у вас есть программа, живущая в Telegram.
Пройдём по лестнице ещё раз, медленно. u := tgbotapi.NewUpdate(0) создаёт конфиг опроса с offset 0 — отдать всё, что копилось; что делает offset, подробно разберём в пятом уроке. u.Timeout = 60 держит HTTP-запрос открытым до минуты, чтобы не бомбить сервер пустыми запросами. bot.GetUpdatesChan(u) запускает внутри библиотеки горутину и возвращает канал: обновления прилетают в for update := range updates, и этот цикл — сердце бота. Первая проверка в теле — update.Message == nil: не каждое обновление содержит сообщение, у нажатий кнопок и правок текста свои поля; пропускаем такие, иначе паника. Из пришедшего сообщения берём update.Message.Chat.ID — куда отвечать — и update.Message.Text — что повторить. Оба поля нужны обязательно: chat_id без текста даёт пустое сообщение, а текст без chat_id не знает адресата.
Как проверять бота вживую#
Запустили — теперь чек-лист проверки из телефона. Пришлите обычный текст: бот должен ответить цитатой. Пришлите эмодзи и стикер: эмодзи приедет текстом, а стикер — обновлением без поля Text, и эхо-бот отправит пустое сообщение с цитатой — полезное наблюдение, объясняющее, почему в будущих уроках перед использованием Text всегда проверяют на пустоту. Отправьте /start: получите его же эхом, о котором говорили выше. Отредактируйте только что отправленное сообщение: бот промолчит — правки живут в другом поле Update. Зайдите с второго устройства или с десктопа: обновления приходят на все сессии одинаково, бот один и тот же. Заодно понаблюдайте за терминалом: каждая отправка бота — это ваш sendMessage, каждое ваше сообщение — чей-то getUpdates, вернувший Update. Пятнадцать минут таких экспериментов дают больше, чем час чтения о Bot API, и все последующие уроки будем проверять именно так.
Кейс: бот-справка для студенческого чата#
Первый заказ, который приносит эхо-шаблон, почти всегда выглядит одинаково: сделать бота, отвечающего на частые вопросы группы. Студсовет, домовой чат, клуб по интересам — логика одна: пришёл вопрос, бот вернул заготовку. Из такого проекта два урока, которых нет в коде. Первый: пишите лог вопросов в файл или базу с первого дня. Через месяц без логов вы не знаете, что реально спрашивают люди, и расширять бота будете вслепую; с логом — берёте топ-десять вопросов и делаете из них команды и клавиатуры из следующих уроков. Второй: у шаблона нет антиспама, и один шутник завалит бота двадцатью сообщениями в минуту — лимиты Telegram и вежливые паузы разбираются в семнадцатом уроке, но помнить о цене молчания стоит уже здесь. Оба урока стоят ноль строк кода на старте и экономят неделю правок потом.
Персонализация: обращение по имени
GOreply := "Ты написал: " + update.Message.Text
if update.Message.From != nil {
reply = update.Message.From.FirstName + ", ты написал: " + update.Message.Text
}
msg := tgbotapi.NewMessage(update.Message.Chat.ID, reply)
msg.ReplyToMessageID = update.Message.MessageID
bot.Send(msg)update.Message.From — указатель на автора: id, FirstName, UserName. Он тоже бывает nil, например у сообщений из каналов, поэтому проверка перед обращением обязательна. FirstName — то имя, что человек указал в профиле, и короткое обращение заметно оживляет бота: один и тот же текст с именем читается совсем иначе. Похожим образом позже будет строиться и вся логика хендлеров: сначала проверки, потом сборка ответа.
ReplyToMessageID: почему бот цитирует вас#
Поле msg.ReplyToMessageID меняет вид ответа: Telegram рисует его с цитатой исходного сообщения, как обычный реплай в приложении. Технически это число — MessageID того сообщения, на которое отвечаем; ноль или отсутствующее поле дают обычное сообщение без цитаты. MessageID уникален внутри одного чата, но не в пределах аккаунта: у одного и того же сообщения в разных чатах идентификаторы разные, поэтому реплай всегда привязан к паре чат-сообщение. Для эхо-бота цитирование — украшение, для сервисных ботов — инструмент: напоминание со ссылкой на задание, ответ на конкретный вопрос в групповом чате, разбор заявки в потоке. Полный набор способов отправки и связывания сообщений разберём в шестом уроке, а здесь важно привыкнуть к паре NewMessage плюс Send: конфиг собирается, отправляется отдельно.
Уберите проверку update.Message == nil и получите самый знаменитый краш первых ботов. Пользователь нажимает inline-кнопку или редактирует своё сообщение — приходит обновление без поля message, и обращение update.Message.Text разыменовывает nil: panic: runtime error: invalid memory address. Процесс падает, бот молчит, пока вы не заметите. Проверка на nil — не перестраховка, а обязательный первый оператор любого хендлера в каждом боте, который вы напишете.
Чего в эхо-боте сознательно нет#
Эхо-бот на Go наивен, и важно видеть границы наивности. Команд он не различает: /start вернётся вам эхом, потому что это тоже просто текст. Состояний нет: бот не помнит, что говорил минуту назад, и не ведёт диалог. Никаких клавиатур и форматирования: чистый текст туда-обратно. Ошибки отправки пишутся в лог, но повторов и очередей нет. Всё это появится позже: команды — в седьмом уроке, reply-клавиатуры — в восьмом, состояния — в десятом. Сейчас ценен скелет: канал обновлений, проверка nil, сборка ответа, отправка с обработкой ошибки — эта четвёрка повторится в любом боте, который вы напишете на Go, от напоминалки до сервиса рассылок. Если разбор каналов и горутин в коде вызывает вопросы, закройте их по продвинутому курсу — там механика расписана с задачами.
Эхо в личке и в группе: разное поведение#
Запустите эхо-бота в группе — и он, скорее всего, замолчит. Это privacy mode: по умолчанию бот в группе видит только команды, адресованные ему, ответы на свои сообщения и сервисные события; свободный текст проходит мимо. Отключается через /setprivacy в BotFather и пере-добавление бота в чат. Подумайте, нужно ли вам это: эхо в общем чате — шум, каждое сообщение получает дубликат с цитатой, и через день группу захочется спасать. В личке эхо безвредно, там и тестируйте. Второе отличие — адресация: в группе update.Message.From.ID — автор сообщения, а Chat.ID — сама группа, и это разные числа. Отвечаете в тот же чат — берите Chat.ID; запоминаете, кто именно написал, — From.ID. В личном диалоге оба совпадают, поэтому о разнице новички узнают только в группе, самое позднее.
Типичная ошибка: медленная обработка внутри цикла#
Цикл из урока разбирает обновления строго по очереди, и это ловушка. Поставьте внутрь запрос к внешнему API на три секунды — и на время каждого запроса бот перестаёт слушать: очередь копится в буферизованном канале библиотеки, а пользователи видят молчание. Один медленный вызов в час незаметен; пробка начинается, когда ботом пользуются одновременно несколько человек. Простейшие средства уже под рукой: вынести медленную часть в горутину — помня про карту состояния и мьютексы из десятого урока, — или отвечать мгновенной заглушкой, а доделывать в фоне. Правильная архитектура для большой нагрузки — webhook из восемнадцатого урока, где обновления приходят HTTP-запросами и обрабатываются параллельно. Пока трафик учебный, достаточно измерять время обработки: одна строка с time.Since в конце цикла покажет, когда заглушки перестают спасать.
Проверка: тип chat_id
Какой тип у update.Message.Chat.ID?
Проверка: NewMessage против Send
Что делает tgbotapi.NewMessage(chatID, text)?
Что ответит эхо-бот на команду
Прочитайте код и выберите, что он выведет:
for update := range updates {
if update.Message == nil {
continue
}
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
msg.ReplyToMessageID = update.Message.MessageID
bot.Send(msg)
}Что увидит бот после правки сообщения
Прочитайте код и выберите, что он выведет:
for update := range updates {
if update.Message == nil {
log.Println("обновление без message")
continue
}
log.Println(update.Message.Text)
}Соберите отправку ответа
Впишите конструктор сообщения, поле реплая и метод отправки.
Соберите сердце эхо-бота
Расставьте строки цикла обновлений в порядке исполнения: от конфига опроса до отправки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Эхо-бот работает, и это уже настоящий бот: живые люди пишут — он отвечает, день и ночь. Дальше углубление в механику: пятый урок вскрывает long polling — что происходит между NewUpdate и сообщением в канале, зачем нужен offset и почему Timeout не декорация. После него — отправка сообщений во всей полноте: chat_id, реплаи, копирование чужих сообщений. Полный маршрут курса — на странице курса. Домашнее задание ниже — на запуск и правку: настоящие боты получаются у тех, кто ломает собственный код руками и читает чужие ошибки без страха.
Карточки: анатомия эхо-бота
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Эхо-бот — полный конвейер: получить Update, проверить Message на nil, собрать NewMessage, отправить bot.Send.
- GetUpdatesChan отдаёт канал обновлений; цикл for range живёт весь срок процесса.
- update.Message равно nil для кнопок и правок; без проверки бот паникует и умирает.
- chat_id — int64 и адресат ответа; MessageID уникален внутри чата и нужен для реплая.
- ReplyToMessageID включает цитирование исходного сообщения; ноль — обычный ответ.
- Эхо-бот не различает команды и не хранит состояние — это каркас для команд, клавиатур и диалогов следующих уроков.
Домашнее задание
Запустите эхо-бота и поиграйте с ним из Telegram: текст, эмодзи, /start, правка своего сообщения. Затем три правки кода по очереди. Первая: уберите проверку update.Message == nil, отредактируйте старое сообщение в чате и наблюдайте панику в терминале, после чего верните проверку. Вторая: замените эхо на приветствие с FirstName из update.Message.From. Третья: закомментируйте ReplyToMessageID и сравните вид ответов. Каждую правку гоняйте отдельно и фиксируйте эффект в комментарии к коду.
Личные заметкиПусто
Вопросы и ответы по уроку
Бот отвечает не на каждое сообщение в группе. Почему?
В группах работает privacy mode: по умолчанию бот видит только команды, адресованные ему, и ответы на свои сообщения. Для эха в группе выключите приватность через /setprivacy в BotFather или проверяйте бота в личном диалоге, где ограничений нет.
Почему в примере ошибка bot.Send проверяется, а в фрагментах нет?
В полной версии эхо-бота проверка есть: if _, err := bot.Send(msg); err != nil { log.Println(err) }. В сокращённых фрагментах её опускают ради читаемости. На проде пропуск проверки превращает молчание бота в необъяснимое — логируйте всегда, хоть одной строкой.
Как остановить бота?
Ctrl+C в терминале: процесс с каналом обновлений убивается сигналом. Неподтверждённые обновления Telegram хранит сутки — после рестарта очередь приедет снова, и сообщения не потеряются. Корректное завершение по сигналу с graceful shutdown разбирается в уроке про деплой.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Мониторинг Go с Prometheus: метрики, Grafana, алерты
Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.
- Понимать четыре типа метрик и выбирать правильный
- Экспортировать метрики из Go через promhttp
- Инструментировать HTTP-запросы и бизнес-события
- Писать PromQL-запросы: rate, sum, p99