---
title: "Медиа: фото, документы и voice"
description: "Фото, документы и voice в telegram-боте на Go: NewPhoto с FileID и FileBytes, PhotoSize из присланного фото, лимиты Bot API. Примеры кода tgbotapi."
course: "Telegram-боты на Go: от эха до полноценного сервиса"
url: "https://golangmaster.ru/courses/telegram-bot/lessons/12-media-i-fajly/"
published: "2026-09-19"
updated: "2026-09-25"
minutes: 18
---

# Медиа: фото, документы и voice

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

Отправка фото в telegram-боте на Go — это NewPhoto и bot.Send, две строки кода, но настоящая сила начинается с file_id: файл, который пользователь уже прислал, не нужно скачивать и перезаливать, он живёт на серверах Telegram и выдаётся по короткой строке. Разберём отправку фото и документов, приём файлов от пользователя и лимиты Bot API на tgbotapi. Отдельно разберём загадочный LOC_ERR, который иногда показывает клиент Telegram рядом с медиа, — и почему он не имеет отношения к коду вашего бота.

Бот, который умеет только текст, быстро упирается в потолок: пользователи шлют фото чеков вместо описания, просят отчёт файлом, а не простынёй текста, и ждут, что бот ответит голосом. 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 стоит сохранить и пользоваться им всегда.

### Два способа отправить одно фото

```go
data, 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, и боту, принимающему любые изображения, надо проверять оба поля.

### Эхо фото: возвращаем присланное

```go
if 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 не переносится между ботами.** Классическая ошибка: скопировать 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 — различия живут внутри библиотеки.

### Отдаём отчёт документом

```go
data, 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 сохранён. Как отправить это фото без повторной загрузки?

- tgbotapi.FileBytes{Bytes: []byte(fileID)}
- ✅ tgbotapi.NewPhoto(chatID, tgbotapi.InputFileString(fileID))
- tgbotapi.NewDocument(chatID, fileID)
- Скопировать file_id в другой бот и отправить оттуда

InputFileString превращает идентификатор в ссылку на уже загруженный файл — NewPhoto отправит его без байтов. FileBytes ждёт содержимое, а не идентификатор, документ даст другой тип сообщения, а file_id не работает у чужого бота.

### Проверка: самый большой размер

**Вопрос:** Как взять file_id фото в максимальном качестве?

- photo[0].FileID — первый всегда лучший
- photo[1].FileID — второй по счёту
- ✅ photo[len(photo)-1].FileID — последний элемент
- Любой: все версии имеют одинаковый file_id

Срез PhotoSize упорядочен от меньшего к большему, последний элемент — оригинал с самым высоким разрешением. Версии отличаются и разрешением, и file_id: превью и оригинал — разные файлы для Telegram.

### Что напечатает разбор среза

```go
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])
```

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

- ✅ 3 true
- 3 false
- 1 true
- 0 true

Для обычного фото срез содержит несколько размеров — три, пять, зависит от картинки, — и ширина первого меньше ширины последнего: срез упорядочен по возрастанию. Проверка на nil перед циклом в бою обязательна, здесь она опущена ради краткости.

### Соберите отправку фото с подписью

Впишите структуру загрузки, поле подписи и конструктор повторной отправки.

```go
cfg := tgbotapi.NewPhoto(chatID,
        tgbotapi.___{Name: "cat.png", Bytes: data})
cfg.___ = "Кот из архива"

again := tgbotapi.NewPhoto(chatID,
        tgbotapi.___(fileID))
```

Подсказка: Байты файла едут в FileBytes с именем и содержимым, подпись — поле Caption, а fileID оборачивается InputFileString.

Правильные ответы в пропусках: `FileBytes`, `Caption`, `InputFileString`

### Шесть строк отправки фото с диска

Расставьте отправку файла с диска: от чтения до обработки ошибки.

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

1. `data, err := os.ReadFile("cat.png")`
2. `cfg := tgbotapi.NewPhoto(chatID, tgbotapi.FileBytes{Name: "cat.png", Bytes: data})`
3. `_, err = bot.Send(cfg)`
4. `if err != nil {`
5. `  log.Println("send:", err)`
6. `}`

### Карточки: медиа и файлы

- **file_id** — Идентификатор файла на серверах Telegram, привязан к боту, получившему файл. Повторная отправка — без байтов, только строка.
- **FileBytes** — Структура {Name, Bytes} для первичной загрузки: бот читает файл с диска и передаёт содержимое в конструктор.
- **PhotoSize** — Одна версия фото: ширина, высота, вес и свой file_id. Срез Photo в сообщении упорядочен от меньшего к большему.
- **NewPhoto против NewDocument** — Фото сжимается и получает превью, документ уходит как есть — правильный выбор для PDF и архивов.
- **Лимиты файлов** — Отправка ботом — до 50 МБ, скачивание через GetFile — до 20 МБ, ссылка из GetFile живёт около часа.
- **file_unique_id** — Постоянная метка файла, одинаковая для всех ботов, но для отправки не годится — только для сравнения и дедупликации.

Медиа добавили боту выразительности, следующий шаг — добавить её тексту: [урок про форматирование](/courses/telegram-bot/lessons/13-formatirovanie/) научит подписи с жирным, курсивом и ссылками через ParseMode. Хранилище file_id из кейса этого урока строится на [карте с мьютексом и Redis из одиннадцатой части](/courses/telegram-bot/lessons/11-hranenie-dannyh/), стикеры и реакции — продолжение темы медиа в [двадцать втором уроке](/courses/telegram-bot/lessons/22-reactions-i-stickery/). Базовая отправка сообщений осталась в [шестом уроке](/courses/telegram-bot/lessons/06-otpravka-soobscheniy/), полный курс — на [его странице](/courses/telegram-bot/).

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

- 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 — код бота менять не нужно.
