Содержание урока · 10 разделов
Справка бота выглядит как стена текста: /help печатает двадцать команд сплошным абзацем, и пользователи перестают её открывать — не видно, где команда, а где пояснение. Проблема не в содержании, а в подаче: Telegram умеет рисовать жирный, курсив, моноширинный код и кликабельные ссылки, но только если бот попросил об этом явно. За это отвечает поле ParseMode у конфигурации сообщения и HTML-разметка внутри текста. Цена включения — новая категория падений: сообщение с тегами проходит разбор на сервере Telegram, и одна лишняя угловая скобка от пользователя уронит его с ошибкой can't parse entities. Хорошая новость: падение локальное, оно не влияет ни на других пользователей, ни на процесс бота — Send просто вернёт ошибку, которую можно обработать. В этом уроке подключим разметку, соберём справку, которую приятно читать, и защитим всё, что бот печатает из пользовательского ввода.
ParseMode и теги HTML#
Telegram поддерживает два режима разметки — HTML и MarkdownV2, — и переключаются они строкой в поле ParseMode. В библиотеке для этого есть константа tgbotapi.ModeHTML со значением HTML: msg.ParseMode = tgbotapi.ModeHTML, и дальше весь текст сообщения разбирается как HTML-фрагмент. Набор тегов намеренно узкий: b и strong дают жирный, i и em — курсив, u подчёркивание, s зачёркивание, code — моноширинный фрагмент, pre — блок кода, a с href — ссылку, blockquote — цитату. Вложенность работает: жирный внутри ссылки, код внутри жирного. Никаких div, span и стилей — сервер примет только перечисленное, остальное либо упадёт, либо уйдёт как текст. Важно понимать механику: теги не рисует библиотека и не рисует ваш Go — они едут в Telegram как часть текста, и там уже превращаются в оформление. Поэтому и экранировать чужие угловые скобки нужно у себя, до отправки.
Справка, которую читают
GOfunc helpMessage() string {
return "<b>Команды бота-дневника</b>\n\n" +
"/today - записи за сегодня\n" +
"/stats - статистика по месяцам\n" +
"/export - выгрузка в файл\n\n" +
"Правила заполнения: <i>коротко, одним сообщением</i>\n" +
"Пример записи: <code>бег 5 км, темп 6:10</code>\n" +
"<a href=\"https://example.com/faq\">Подробная справка</a>"
}
msg := tgbotapi.NewMessage(chatID, helpMessage())
msg.ParseMode = tgbotapi.ModeHTML
bot.Send(msg)Разметка живёт прямо в строке текста, переключатель — одна строка ParseMode. Заголовок жирным, пояснение курсивом, пример записи кодом, ссылка уводит на сайт. Пустые строки между секциями дают текстовые разделители, и вся справка читается за секунды, а не пролистывается.
Экранирование пользовательского ввода#
Пока текст сообщения написан вами, теги безопасны. Как только бот вставляет в разметку то, что прислал пользователь, появляется вторая сторона медали: строка с угловой скобкой ломает разбор. Классический сценарий — эхо-бот с ParseMode: пользователь пишет «люблю <3» или «2 < 3 и 5 > 4», и сервер отвечает ошибкой can't parse entities, сообщение не уходит вовсе. Ошибка приходит на конкретное сообщение: остальные отправки при этом работают, и в логах бот выглядит здоровым — падение видно только по команде конкретного пользователя. Лечение — html.EscapeString из стандартной библиотеки: функция превращает угловые скобки, амперсанд и кавычки в HTML-сущности, которые Telegram показывает как текст, а не читает как разметку. Правило разделения труда простое: свою разметку экранировать нельзя — жирный перестанет быть жирным, — а весь пользовательский ввод экранируется всегда, до склейки со строкой. Из этого следует порядок сборки: сначала экранируем ввод, потом обкладываем тегами, и никогда наоборот.
Безопасное эхо с разметкой
GOfunc safeEcho(update tgbotapi.Update) string {
name := html.EscapeString(update.Message.From.FirstName)
text := html.EscapeString(update.Message.Text)
return fmt.Sprintf("<b>%s</b> написал:\n%s", name, text)
}
msg := tgbotapi.NewMessage(chatID, safeEcho(update))
msg.ParseMode = tgbotapi.ModeHTML
bot.Send(msg)Оба куска пользовательского ввода — имя и текст — проходят через html.EscapeString до попадания в шаблон. Тег b при этом остаётся живым: он написан нами после экранирования. Строка «люблю <3» дойдёт как текст, и can't parse entities не случится никогда.
Симптом: сообщение с разметкой не отправляется, Send возвращает Bad Request: can't parse entities. Диагноз: в тексте неэкранированный символ HTML — чаще всего угловая скобка или голый амперсанд. Лечение: html.EscapeString на весь пользовательский ввод и проверка своей разметки на незакрытые теги. Ошибка не зависит от длины: одно «<3» роняет сообщение так же надёжно, как сломанный тег.
HTML против MarkdownV2#
Почему в уроках курса HTML, если Markdown привычнее? Аргументы за MarkdownV2 — короткая запись: два символа обёртки вместо пары тегов. Аргументы против — экранирование: MarkdownV2 требует маскировать целую дюжину символов в пользовательском тексте — подчёркивания, звёздочки, скобки всех видов, дефисы в некоторых позициях, — и забытый символ даёт ту же can't parse entities. HTML-режим маскирует ровно три символа: скобку, амперсанд и кавычку, и делает это готовая функция стандартной библиотеки. Второй аргумент — строгость: HTML разбирается предсказуемо, а парсер Markdown в Telegram чувствителен к пробелам и переносу строк. Спорное на практике звучит так: если бот только ваш текст пишет без вставок — берите что нравится, разницы почти нет. Как только в шаблоне появляется пользовательский ввод, HTML с html.EscapeString экономит часы отладки, и я не видел обратных случаев. Переключение между режимами стоит ровно одну строку — поле ParseMode переприсваивается, — поэтому решение легко пересмотреть, если проект срастётся с вводом.
Кейс: карточка записи в боте-дневнике#
Бот-дневник из прошлых уроков подтверждает сохранённую запись карточкой: дата курсивом, сама запись как есть, счётчик серии жирным. Разметка собирается конкатенацией: date := record.Date.Format("02.01"), затем fmt.Sprintf("<i>%s</i>\n%s\n<b>Серия: %d дней</b>", date, html.EscapeString(record.Text), record.Streak). Три строки кода, а карточка выглядит живой — и в истории чата её видно с первого взгляда. Здесь работает тот же принцип, что и с callback_data из девятого урока: машинное и человеческое разведены. Хранение остаётся плоским текстом, разметка — слой отображения, и если однажды понадобится экспорт в другой канал, данные не придётся чистить от тегов. Захардкоженные в базу теги — типичный след новичка, который потом всплывает в CSV-выгрузках и уезжает на сайт вместе с браком.
Две ловушки: незакрытый тег и блок pre#
Вторая категория падений случается без всякого ввода: бот собирает сообщение кусками и теряет закрывающий тег. Открывающий b есть, закрывающий забыт — и сервер либо ругается can't parse entities, либо оформляет жирным всё до конца сообщения: вместе с подписью и контактами. Лечится это дисциплиной сборки: разметку кусков собирают функциями, у каждой пары открывающий-закрывающий тег живёт в одной строке, а сообщение собирается конкатенацией готовых кусков. Полезная привычка — тестовая отправка самому себе в личку перед релизом: один ваш chat_id как первый получатель ловит сломанную разметку раньше пользователей. Обратная крайность — двойное экранирование, когда про html.EscapeString вспоминают дважды: пользователь видит <b> вместо жирного. Проверка простая: экранирует функция у границы ввода, ровно один раз, а не в каждом слое.
Служебные боты часто возвращают куски логов, диффы и таблицы — и тут раскрывается тег pre: многострочный блок моноширинным с сохранением переносов, читается как терминал. Нюанс тот же, что и с обычным вводом: строки лога полны угловых скобок и амперсандов, поэтому содержимое блока тоже проходит через html.EscapeString, и только потом обкладывается pre. Формат лучше делать коротким строкам — Telegram рисует блок без переносов, и длинная строка уезжает за экран телефона. Для больших отчётов честнее документ из урока про медиа — файл не упирается в лимит текстового сообщения, а pre остаётся для коротких выдержек в диалоге.
Конкатенация строк, fmt.Sprintf и html.EscapeString — чистый Go, и разницу «до и после экранирования» видно прямо в песочнице на строке с угловой скобкой. Живую разметку проверяет только настоящий Telegram: локальный Go, реальный токен и сообщение с <b>приветом</b> в личке бота.
Теги, которые понимает Telegram#
| Тег | Что делает | Где уместен |
|---|---|---|
| b, strong | жирный | заголовки секций, акценты |
| i, em | курсив | даты, пояснения, ремарки |
| code | моноширинный фрагмент | команды, значения, числа |
| pre | блок кода с рамкой | многострочные примеры |
| a href | кликабельная ссылка | справка, внешние страницы |
| blockquote | цитата с отступом | цитаты сообщений, отзывы |
Спорное мнение: разметка не для каждого сообщения#
Освоив теги, новички обкладывают ими весь трафик бота: эхо с жирным, подтверждения курсивом, всё как в лендинге. Я считаю это ошибкой вкуса с реальной ценой: разметка уместна там, где есть структура — справка, карточки, отчёты, — и вредна в коротких диалоговых репликах, где она шумит и добавляет каждое сообщению пафос объявления. Граница рабочая: если сообщение длиннее трёх строк или содержит перечисление — разметка помогает; если это ответ «принял» или следующий вопрос анкеты — плоский текст честнее. Второй аргумент экономный: каждое сообщение с ParseMode проходит разбор на сервере, и баг разметки роняет его целиком, тогда как плоский текст сломать нечем. Третий — спокойствие пользователя: чат, в котором каждое второе сообщение оформлено как баннер, читается утомительно, и люди быстрее отключают уведомления. Разметка — приправа, а не блюдо; это правило не из документации, но из поддержки ботов следует напрямую.
Проверка: включение HTML
Как включить HTML-разметку у сообщения в tgbotapi?
Проверка: упавшее сообщение
Бот с ParseMode HTML вставил в сообщение текст пользователя «2 < 3». Что произойдёт?
Что увидит пользователь
Прочитайте код и выберите, что он выведет:
msg := tgbotapi.NewMessage(chatID,
"<b>Привет</b>, это бот")
// ParseMode не установлен
bot.Send(msg)Соберите разметку безопасно
Впишите поле разметки, константу режима и функцию экранирования.
Шесть строк отправки с разметкой
Расставьте отправку сообщения с HTML: от конструктора до обработки ошибки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: форматирование сообщений
Текст бота приобрёл структуру — теперь ему нужен масштаб: урок про рассылки разошлёт разметку сотням подписчиков с паузами и обработкой ошибок, а команды и меню из седьмой части станут местом, где справка с HTML живёт постоянно. Лимиты API и вежливые паузы подробнее разбираются в уроке про ошибки и лимиты. Экранирование строк опирается на устройство строк в Go — оно есть в базовом курсе, а карта всех уроков — на странице курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Разметка включается полем ParseMode: tgbotapi.ModeHTML включает HTML-режим у сообщения.
- Набор тегов узкий: b, i, u, s, code, pre, a, blockquote — без div и стилей.
- Пользовательский ввод экранируется html.EscapeString до склейки с шаблоном: скобка в тексте роняет сообщение с can't parse entities.
- Свою разметку экранировать нельзя — экранируют только вставки из ввода, и всегда до обёртки тегами.
- HTML практичнее MarkdownV2 там, где есть пользовательский ввод: маскировать надо три символа, а не дюжину.
- Без ParseMode теги печатаются как текст — ошибки нет, но пользователь видит скобки.
- Разметка уместна в справках и карточках; короткие диалоговые реплики честнее оставлять плоскими.
Домашнее задание
Перепишите /help локального бота на HTML: заголовок жирным, каждая команда отдельной строкой с кодом, ссылка на страницу справки. Затем добавьте эхо с разметкой и проверьте два ввода: обычную строку и «2 < 3 и 5 > 4» — второй должен дойти как текст, а не уронить Send. Ошибку can't parse entities поймайте обратным способом: закомментируйте экранирование и посмотрите ответ API.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему Markdown-режим в библиотеке называется MarkdownV2 и чем он отличается?
Старый режим Markdown в Bot API объявлен устаревшим: актуальный называется MarkdownV2 и строже в экранировании. Библиотека держит константу ModeMarkdownV2, но для ботов с пользовательским вводом HTML обычно удобнее.
Можно ли комбинировать разметку с кнопками?
Да: ReplyMarkup и ParseMode независимы, сообщение с inline-клавиатурой спокойно несёт жирные заголовки и ссылки. Это стандартная пара для карточек с кнопками действий.
Какой длины может быть подпись с разметкой?
Лимит считается по итоговой строке: 4096 символов на текстовое сообщение, 1024 на подпись медиа — после разбора разметки. Экранирование раздувает строку, поэтому на грани лимита оставляйте запас.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Рассылки: уведомления подписчикам
- Inline-режим: бот в поле поиска
- Редактирование сообщений и многостраничное меню
- Ошибки и лимиты Telegram: 429 и RetryAfter
- Медиа: фото, документы и voice
- Хранение данных бота: память и Redis
- Состояние диалога: анкета по шагам
- Inline-клавиатуры и callback-запросы
- Все курсы по Go
Следующая ступень учебного пути
Мониторинг Go с Prometheus: метрики, Grafana, алерты
Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.
- Понимать четыре типа метрик и выбирать правильный
- Экспортировать метрики из Go через promhttp
- Инструментировать HTTP-запросы и бизнес-события
- Писать PromQL-запросы: rate, sum, p99