---
title: "Форматирование: HTML в сообщениях"
description: "Форматирование сообщений telegram-бота: ParseMode HTML в tgbotapi, теги b, i, code и ссылки, экранирование ввода — и ошибка can't parse entities."
course: "Telegram-боты на Go: от эха до полноценного сервиса"
url: "https://golangmaster.ru/courses/telegram-bot/lessons/13-formatirovanie/"
published: "2026-09-19"
updated: "2026-09-25"
minutes: 16
---

# Форматирование: HTML в сообщениях

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

Форматирование в telegram-боте включается одним полем — msg.ParseMode = tgbotapi.ModeHTML, после чего в тексте работают теги жирного, курсива, кода и ссылок. Здесь разберём HTML-режим tgbotapi и его теги, экранирование пользовательского ввода через html.EscapeString и ошибку can't parse entities, которая валит сообщения из-за одного символа.

Справка бота выглядит как стена текста: /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 как часть текста, и там уже превращаются в оформление. Поэтому и экранировать чужие угловые скобки нужно у себя, до отправки.

### Справка, которую читают

```go
func 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 показывает как текст, а не читает как разметку. Правило разделения труда простое: свою разметку экранировать нельзя — жирный перестанет быть жирным, — а весь пользовательский ввод экранируется всегда, до склейки со строкой. Из этого следует порядок сборки: сначала экранируем ввод, потом обкладываем тегами, и никогда наоборот.

### Безопасное эхо с разметкой

```go
func 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 не случится никогда.

> **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 вспоминают дважды: пользователь видит &lt;b&gt; вместо жирного. Проверка простая: экранирует функция у границы ввода, ровно один раз, а не в каждом слое.

Служебные боты часто возвращают куски логов, диффы и таблицы — и тут раскрывается тег 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?

- msg.Text = tgbotapi.ModeHTML
- ✅ msg.ParseMode = tgbotapi.ModeHTML
- tgbotapi.NewMessage(chatID, "<html>...")
- HTML включён по умолчанию для всех сообщений

ParseMode — поле конфигурации, в которое кладётся константа tgbotapi.ModeHTML. Без него теги едут в чат как обычный текст: пользователь увидит <b>жирный</b> вместе со скобками.

### Проверка: упавшее сообщение

**Вопрос:** Бот с ParseMode HTML вставил в сообщение текст пользователя «2 < 3». Что произойдёт?

- Telegram покажет текст как есть
- Скобка исчезнет из сообщения
- ✅ Send вернёт ошибку can't parse entities
- Бот упадёт с паникой

Неэкранированная угловая скобка ломает разбор HTML на сервере, и API возвращает Bad Request. Спасает html.EscapeString на пользовательском вводе до сборки шаблона — тогда скобка превращается в безобидную сущность.

### Что увидит пользователь

```go
msg := tgbotapi.NewMessage(chatID,
        "<b>Привет</b>, это бот")
// ParseMode не установлен
bot.Send(msg)
```

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

- Привет — жирным, остальное обычным
- ✅ Текст <b>Привет</b>, это бот — вместе со скобками
- Сообщение не отправится: ошибка разметки
- Только Привет, без остального

Без ParseMode Telegram не разбирает разметку и печатает строку как есть — теги видны пользователю. Ошибки при этом не будет: текст валидный, просто оформлен буквально. Включение ModeHTML превращает теги в оформление.

### Соберите разметку безопасно

Впишите поле разметки, константу режима и функцию экранирования.

```go
msg := tgbotapi.NewMessage(chatID, text)
msg.___ = tgbotapi.___
name := html.___(update.Message.From.FirstName)
```

Подсказка: Поле называется ParseMode, константа режима — ModeHTML, а ввод экранирует html.EscapeString из стандартной библиотеки.

Правильные ответы в пропусках: `ParseMode`, `ModeHTML`, `EscapeString`

### Шесть строк отправки с разметкой

Расставьте отправку сообщения с HTML: от конструктора до обработки ошибки.

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

1. `msg := tgbotapi.NewMessage(chatID, "<b>Готово</b>")`
2. `msg.ParseMode = tgbotapi.ModeHTML`
3. `_, err := bot.Send(msg)`
4. `if err != nil {`
5. `  log.Println("send:", err)`
6. `}`

### Карточки: форматирование сообщений

- **ParseMode** — Поле конфигурации сообщения, включающее разбор разметки: tgbotapi.ModeHTML или MarkdownV2. Без него теги печатаются как текст.
- **tgbotapi.ModeHTML** — Константа со значением HTML: включает HTML-режим с тегами b, i, u, s, code, pre, a, blockquote.
- **html.EscapeString** — Функция стандартной библиотеки: превращает скобки, амперсанд и кавычки в сущности. Обязательна для любого пользовательского ввода в разметке.
- **can't parse entities** — Ошибка Bot API на сломанную разметку: неэкранированный символ или незакрытый тег. Сообщение не отправляется.
- **MarkdownV2** — Второй режим разметки: запись короче, но требует маскировать дюжину символов в пользовательском тексте. Для ботов с вводом неудобен.
- **Порядок сборки** — Сначала экранируем ввод, потом оборачиваем своими тегами. Наоборот — значит сломать и разметку, и ввод одновременно.

Текст бота приобрёл структуру — теперь ему нужен масштаб: [урок про рассылки](/courses/telegram-bot/lessons/14-rassylki/) разошлёт разметку сотням подписчиков с паузами и обработкой ошибок, а [команды и меню](/courses/telegram-bot/lessons/07-komandy-bota/) из седьмой части станут местом, где справка с HTML живёт постоянно. Лимиты API и вежливые паузы подробнее разбираются в [уроке про ошибки и лимиты](/courses/telegram-bot/lessons/17-oshibki-i-limity/). Экранирование строк опирается на устройство строк в Go — оно есть в [базовом курсе](/courses/go-basics/lessons/06-stroki-i-runes/), а карта всех уроков — на [странице курса](/courses/telegram-bot/).

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

- Разметка включается полем 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 на подпись медиа — после разбора разметки. Экранирование раздувает строку, поэтому на грани лимита оставляйте запас.
