Бот, который умеет только текст, быстро упирается в потолок: пользователи шлют фото чеков вместо описания, просят отчёт файлом, а не простынёй текста, и ждут, что бот ответит голосом. Telegram передаёт пять основных видов контента — фото, видео, документы, аудио и голосовые, — и у каждого в библиотеке свой конструктор с одинаковой логикой. Общая идея у всех одна: файл либо загружается ботом впервые, либо уже лежит на серверах Telegram, и тогда вместо данных передаётся его идентификатор. Второй случай важнее и экономнее, поэтому урок построен вокруг него: сначала отправим фото двумя способами, потом научимся принимать фото от пользователя и забирать у него file_id, а в конце — отдавать документы и честно поговорим про лимиты, которые у Bot API на удивление скромные. Весь код урока работает в личке и в группе одинаково, отличия только в том, чей ID использовать для ответа — эта развилка разобрана ещё в шестом уроке, здесь она просто соблюдается. К концу урока бот примет и вернёт фото, отдаст PDF и узнает про медиа-группы.
Отправка фото: file_id против загрузки#
NewPhoto принимает два аргумента — chat_id и файл, а файл может быть трёх видов. InputFileString(fileID) — строка-идентификатор: Telegram уже хранит этот файл, и новая загрузка не нужна. FileBytes — структура с полями Name и Bytes: бот читает файл с диска и загружает его, отправляя байты на сервер. Третий путь — URL: NewPhoto умеет передавать ссылку, и Telegram скачает картинку сам, без участия вашего кода. К каждому фото прикрепляется подпись: поле Caption у PhotoConfig, до 1024 символов по документации Bot API. Правило выбора простое: если файл уже приходил боту или вы отправляете его повторно — только file_id, загрузка байтов это трата трафика и секунд ожидания. Загрузка нужна один раз — при первом знакомстве бота с файлом, после чего его file_id стоит сохранить и пользоваться им всегда.
Два способа отправить одно фото
GOdata, err := os.ReadFile("cat.png")
if err != nil {
log.Println(err)
return
}
first := tgbotapi.NewPhoto(chatID,
tgbotapi.FileBytes{Name: "cat.png", Bytes: data})
first.Caption = "Кот из архива, первая отправка"
bot.Send(first)
second := tgbotapi.NewPhoto(chatID, tgbotapi.InputFileString(fileID))
second.Caption = "Тот же кот, без повторной загрузки"
bot.Send(second)Первая отправка несёт байты файла: FileBytes с именем и содержимым, прочитанным os.ReadFile. Telegram сохраняет файл и в ответе присылает его fileID, который стоит сохранить. Вторая отправка идёт по InputFileString — тот же NewPhoto, но файл не передаётся повторно, сработает и на медленном канале, и при большом размере.
Как бот принимает фото от пользователя#
Пользователь отправил фото — апдейт приходит с Message, у которого поле Photo заполнено. Нюанс, о котором спотыкаются все: Photo это срез PhotoSize, а не один размер. Telegram хранит несколько версий картинки — превью, средний размер, оригинал, — и каждая версия имеет свой file_id, ширину, высоту и вес. Крупнейший вариант всегда последний элемент среза, и его file_id и нужен боту: photo[len(photo)-1].FileID. У пустого сообщения Photo равен nil, и проверка на nil перед обращением к срезу обязательна — иначе паника на первом же стикере вместо фото. Обратная дорога короткая: полученный fileID передаётся в NewPhoto через InputFileString, и бот вернёт пользователю его же картинку. Для ответов в группах работает то же правило, что и раньше: отправлять надо в Chat.ID, а личность автора смотреть в From. Отдельный случай — фото прислали без сжатия, как документ: тогда оно лежит в Message.Document, а Photo равен nil, и боту, принимающему любые изображения, надо проверять оба поля.
Эхо фото: возвращаем присланное
GOif update.Message == nil {
return
}
photo := update.Message.Photo
if photo == nil {
return
}
largest := photo[len(photo)-1]
cfg := tgbotapi.NewPhoto(update.Message.Chat.ID,
tgbotapi.InputFileString(largest.FileID))
cfg.Caption = fmt.Sprintf("Принято, %dx%d",
largest.Width, largest.Height)
bot.Send(cfg)Три проверки перед работой: сообщение существует, это именно фото, срез не пуст. Крупнейший вариант — последний элемент среза, его FileID уходит в InputFileString. Подпись собирает размеры из Width и Height. На присланное фото бот отвечает тем же фото без единого байта загрузки — вся дорога через идентификатор.
Классическая ошибка: скопировать file_id из одного бота в другой и получить Broken file identifier. Идентификатор привязан к токену бота, который его получил, и чужому боту бесполезен. Не путайте и с file_unique_id — он постоянен и универсален, но для отправки не годится, это только метка для сравнения. Дёшево переносить между ботами можно только сами байты файла.
Документы и voice#
Документ — тот же механизм с другим конструктором: NewDocument(chatID, file) отправляет файл как есть, без сжатия и превью, и это правильный путь для PDF, таблиц и архивов. Отличие от фото принципиально: фото Telegram предлагает сжать и показать превью, документ сохраняет байты нетронутыми. Голосовые — NewVoice: файл в формате OGG с кодеком Opus, Telegram покажет его как аудиосообщение с волновой дорожкой; обычный MP3 голосовым не станет — уедет как аудиофайл через SendAudio. По лимитам Bot API честная картина такая: отправить бот может файл до 50 МБ, а вот скачать присланное пользователем через GetFile — до 20 МБ, и это ограничение регулярно удивляет. Ссылка на скачивание из GetFile живёт около часа, поэтому качать надо сразу, а не класть URL в базу на потом. Отправка всех трёх типов собирается одинаково: конструктор, поле Caption при желании, bot.Send — различия живут внутри библиотеки.
Отдаём отчёт документом
GOdata, err := os.ReadFile("report-march.pdf")
if err != nil {
log.Println("read:", err)
return
}
doc := tgbotapi.NewDocument(chatID,
tgbotapi.FileBytes{Name: "report-march.pdf", Bytes: data})
doc.Caption = "Отчёт за март. Вопросы - в /help"
bot.Send(doc)Документ собирается так же, как фото: байты и имя, под которым пользователь увидит файл. Caption с подписью не обязателен, но с ним отчёт находится в истории чата поиском. Отправка через bot.Send единая для всех типов — конструктор выбирает метод API за вас.
Кейс: бот-архив чеков#
Реальный бот-помощник бухгалтерии принимает фото чеков и возвращает их по запросу «покажи март». Решение на Material из прошлого урока: при получении фото бот берёт file_id крупнейшего размера и сохраняет его вместе с датой в хранилище — ключ bot:receipt:100, значение JSON со списком идентификаторов. По запросу бот собирает ответы NewPhoto по InputFileString за доли секунды, потому что ни одного файла не передаёт. Telegram здесь работает как бесплатный CDN: хранение, раздача и резервные копии уже оплачены самим фактом существования Bot API. Обратная сторона — file_id уникален для бота, и при смене токена архив придётся перезагружать заново байтами. Для учебного проекта это приемлемая цена, для боевого — повод держать копии файлов у себя, хотя бы в момент первого приёма. Подпись у каждого возвращённого чека возвращает месяц и сумму — ровно те метаданные, что сохранялись рядом с file_id.
Логику выбора крупнейшего PhotoSize можно тренировать в песочнице — это обычные срезы и структуры. Живая отправка требует локального Go, токена из урока про BotFather и телефона: превью, подписи и голосовые существуют только в настоящем Telegram, curl их не покажет.
Что чем отправлять#
| Контент | Конструктор | Нюанс |
|---|---|---|
| Фото | NewPhoto | сжатие и превью; FileID — без загрузки |
| Документ | NewDocument | без сжатия, подходит для PDF и таблиц |
| Voice | NewVoice | OGG с кодеком Opus, аудиосообщение |
| Повторная отправка | InputFileString(fileID) | файл уже на серверах, байты не нужны |
| Первичная загрузка | FileBytes{Name, Bytes} | файл читается с диска бота |
Экономика файлов: байты против file_id#
Бот-каталог отправлял одну и ту же картинку товара при каждом запросе, и код честно читал os.ReadFile и грузил байты заново. Работало — пока пользователь с мобильным интернетом не начал жаловаться на секундные паузы: каждая отправка несла полный файл по сети туда и обратно. Лечение в одну строку: при первой отправке bot.Send возвращает Message, в котором лежит Photo с file_id отправленного файла, — сохраните его в хранилище из прошлого урока, и все дальнейшие отправки пойдут через InputFileString. Разница в скорости — на порядок, потому что Telegram отдаёт файл со своих серверов без вашего участия. Вторая половина ошибки — память: если картинок много, а байты лежат в переменных, бот незаметно съедает гигабайты оперативки; file_id весит десятки байт, и это правильная единица хранения.
Моё рабочее правило для ботов без особых требований: файлы живут в Telegram, у бота только file_id и метаданные. Против него обычно выступает страх «а вдруг Telegram удалит» — но за годы практики я не встретил случая, чтобы файл, присланный боту, исчез из истории чата. Границы, где мнение сдаётся, конкретные: файл больше 20 МБ, который надо качать и обрабатывать; приватные данные с требованием локального хранения; и смена токена бота, после которой идентификаторы умирают. Во всех трёх случаях нужны собственные копии. В остальных — повторная загрузка байтов это время, трафик и место на диске, потраченные без причины.
Проверка: повторная отправка
Пользователь уже присылал боту фото, fileID сохранён. Как отправить это фото без повторной загрузки?
Проверка: самый большой размер
Как взять file_id фото в максимальном качестве?
Что напечатает разбор среза
Прочитайте код и выберите, что он выведет:
photo := update.Message.Photo
var widths []int
for _, p := range photo {
widths = append(widths, p.Width)
}
fmt.Println(len(photo), widths[0] < widths[len(widths)-1])Соберите отправку фото с подписью
Впишите структуру загрузки, поле подписи и конструктор повторной отправки.
Шесть строк отправки фото с диска
Расставьте отправку файла с диска: от чтения до обработки ошибки.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: медиа и файлы
Медиа добавили боту выразительности, следующий шаг — добавить её тексту: урок про форматирование научит подписи с жирным, курсивом и ссылками через ParseMode. Хранилище file_id из кейса этого урока строится на карте с мьютексом и Redis из одиннадцатой части, стикеры и реакции — продолжение темы медиа в двадцать втором уроке. Базовая отправка сообщений осталась в шестом уроке, полный курс — на его странице.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- NewPhoto принимает три вида файлов: InputFileString для уже загруженного, FileBytes для загрузки с диска, URL для ссылки.
- file_id экономит трафик и время: файл, который уже приходил боту, не нужно перезаливать.
- Photo в сообщении — срез PhotoSize от меньшего к большему; оригинал — последний элемент, у каждого размера свой file_id.
- file_id привязан к токену бота: между ботами не переносится, file_unique_id универсален, но только для сравнения.
- Документы — NewDocument без сжатия, voice — NewVoice с OGG/Opus.
- Лимиты Bot API: отправка до 50 МБ, скачивание через GetFile до 20 МБ, ссылка живёт около часа.
- Telegram работает как бесплатный CDN бота, пока не потребовались свои копии — границу задают приватность и смена токена.
Домашнее задание
Научите локального бота принимать фото и возвращать его же с подписью с размерами в пикселях. Затем добавьте команду /report, которая отправляет любой файл с диска документом, и сохраните file_id первого присланного фото в хранилище из одиннадцатого урока: команда /again должна вернуть это фото без повторной загрузки.
Личные заметкиПусто
Вопросы и ответы по уроку
Почему Photo иногда nil, хотя пользователь прислал картинку?
Картинка могла прийти сжатием группы или как документ — тогда заполнено Message.Document вместо Photo. Проверяйте оба поля, если бот должен принимать любые изображения.
Можно ли отправить фото по ссылке из интернета?
Да: NewPhoto принимает URL-строку, Telegram скачает и разошлёт картинку сам. Ограничение — 5 МБ для фото по URL и требование, чтобы ссылка отдавала контент напрямую, а не HTML-страницу.
Как скачать фото, которое прислал пользователь?
Через GetFile по file_id: библиотека вернёт объект File, у него есть метод Link — прямая ссылка для http.Get. Лимит 20 МБ на файл и час жизни ссылки, качать надо сразу.
Что значит LOC_ERR в Telegram — это ошибка моего бота?
Нет, бот ни при чём: LOC_ERR (например, «LOC_ERR: Members_many») — это сбой локализации клиента Telegram, который показывает служебный ключ вместо переведённой строки. Встречается в сторонних клиентах и старых версиях официального приложения, чаще у медиа-сообщений и счётчиков участников. Лечится обновлением официального Telegram — код бота менять не нужно.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
- Форматирование: HTML в сообщениях
- Рассылки: уведомления подписчикам
- Inline-режим: бот в поле поиска
- Редактирование сообщений и многостраничное меню
- Хранение данных бота: память и Redis
- Состояние диалога: анкета по шагам
- Inline-клавиатуры и callback-запросы
- Reply-клавиатуры: кнопки вместо ввода
- Все курсы по Go
Следующая ступень учебного пути
Мониторинг Go с Prometheus: метрики, Grafana, алерты
Курс по метрикам в Go: Counter, Gauge, Histogram, лейблы, PromQL, Grafana-дашборды, алерты и бизнес-метрики. 25 уроков с примерами client_golang.
- Понимать четыре типа метрик и выбирать правильный
- Экспортировать метрики из Go через promhttp
- Инструментировать HTTP-запросы и бизнес-события
- Писать PromQL-запросы: rate, sum, p99