---
title: "Первый бот: эхо-собеседник"
description: "Первый телеграм бот на Go: эхо-бот в 30 строк — GetUpdates, NewMessage, ReplyToMessageID и bot.Send. Построчный разбор и ошибки новичка с проверками."
course: "Telegram-боты на Go: от эха до полноценного сервиса"
url: "https://golangmaster.ru/courses/telegram-bot/lessons/04-pervyj-bot-echo/"
published: "2026-09-19"
updated: "2026-09-25"
minutes: 18
---

# Первый бот: эхо-собеседник

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

Эхо-бот на Go — классический первый телеграм бот: он повторяет каждое ваше сообщение и отвечает на него реплаем. Уложимся в тридцать строк: GetUpdates, NewMessage, ReplyToMessageID и bot.Send — построчно, с разбором ошибок, на которых ломаются все новички.

Эхо-бот — hello world этой области: что человек написал, то бот и вернул, да ещё с цитированием. Схема тривиальная, но проходит через весь конвейер настоящего бота: получить обновление, проверить, что в нём сообщение, достать текст и chat_id, собрать ответ, отправить, обработать ошибку отправки. Каждый элемент этой цепочки в следующих уроках вырастет в отдельную тему, поэтому эхо-бот полезнее, чем кажется: это работающий каркас, к которому дальше прикручиваются команды, клавиатуры и состояния диалога. Именно поэтому первый телеграм бот стоит писать руками, а не копировать: пальцы запоминают порядок проверок лучше глаз. Приготовьте терминал: токен лежит в переменной окружения, модуль из третьего урока собран, библиотека установлена. Запускать будем локально — песочница сайта без сети, а боту она нужна постоянно.

### Первый телеграм бот: полный код эхо-бота

Вот весь эхо-бот. Не пугайтесь объёма: треть здесь — импорты и обработка ошибок, которая в учебных примерах часто выброшена, а в рабочем коде обязательна. Скопируйте в main.go, подставьте переменную окружения с токеном и запустите, затем напишите боту что-нибудь из Telegram — и он ответит цитатой вашего сообщения. Разбор каждой строки идёт сразу после кода, а после него — две правки, которые превратят безликое эхо в живого собеседника.

### main.go эхо-бота целиком

```go
package 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 и вежливые паузы разбираются в семнадцатом уроке, но помнить о цене молчания стоит уже здесь. Оба урока стоят ноль строк кода на старте и экономят неделю правок потом.

### Персонализация: обращение по имени

```go
reply := "Ты написал: " + 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 уникален внутри одного чата, но не в пределах аккаунта: у одного и того же сообщения в разных чатах идентификаторы разные, поэтому реплай всегда привязан к паре чат-сообщение. Для эхо-бота цитирование — украшение, для сервисных ботов — инструмент: напоминание со ссылкой на задание, ответ на конкретный вопрос в групповом чате, разбор заявки в потоке. Полный набор способов отправки и связывания сообщений разберём в [шестом уроке](/courses/telegram-bot/lessons/06-otpravka-soobscheniy/), а здесь важно привыкнуть к паре NewMessage плюс Send: конфиг собирается, отправляется отдельно.

> **Пропустили nil — бот умирает на кнопке.** Уберите проверку update.Message == nil и получите самый знаменитый краш первых ботов. Пользователь нажимает inline-кнопку или редактирует своё сообщение — приходит обновление без поля message, и обращение update.Message.Text разыменовывает nil: panic: runtime error: invalid memory address. Процесс падает, бот молчит, пока вы не заметите. Проверка на nil — не перестраховка, а обязательный первый оператор любого хендлера в каждом боте, который вы напишете.

### Чего в эхо-боте сознательно нет

Эхо-бот на Go наивен, и важно видеть границы наивности. Команд он не различает: /start вернётся вам эхом, потому что это тоже просто текст. Состояний нет: бот не помнит, что говорил минуту назад, и не ведёт диалог. Никаких клавиатур и форматирования: чистый текст туда-обратно. Ошибки отправки пишутся в лог, но повторов и очередей нет. Всё это появится позже: команды — в [седьмом уроке](/courses/telegram-bot/lessons/07-komandy-bota/), reply-клавиатуры — в восьмом, состояния — в десятом. Сейчас ценен скелет: канал обновлений, проверка nil, сборка ответа, отправка с обработкой ошибки — эта четвёрка повторится в любом боте, который вы напишете на Go, от напоминалки до сервиса рассылок. Если разбор каналов и горутин в коде вызывает вопросы, закройте их по [продвинутому курсу](/courses/go-advanced/) — там механика расписана с задачами.

### Эхо в личке и в группе: разное поведение

Запустите эхо-бота в группе — и он, скорее всего, замолчит. Это 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?

- int
- ✅ int64
- string
- uint32

chat_id — int64: идентификаторы чатов выходят за диапазон int32, и API везде ждёт 64-битное число. Привыкайте к int64 сразу — переполнение на больших группах ловится болезненно.

### Проверка: NewMessage против Send

**Вопрос:** Что делает tgbotapi.NewMessage(chatID, text)?

- Немедленно отправляет сообщение в чат
- ✅ Создаёт MessageConfig — заготовку, которую отправляет bot.Send
- Читает последнее сообщение чата
- Создаёт новый чат с пользователем

NewMessage только конструирует конфиг с полями ChatID и Text. Отправку выполняет bot.Send(msg) — разделение удобно: между созданием и отправкой навешиваются ReplyToMessageID, клавиатуры и ParseMode.

### Что ответит эхо-бот на команду

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

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

- Приветствие и список команд
- Ничего: команды эхо-бот не обрабатывает
- ✅ Эхо: сообщение с текстом /start, с цитатой
- Ошибку: команда пришла в другом поле

Для эхо-бота /start — обычный текст: поле Text содержит строку /start, и бот вернёт её же реплаем. Различать команды бот научится в седьмом уроке через update.Message.Command().

### Что увидит бот после правки сообщения

```go
for update := range updates {
        if update.Message == nil {
                log.Println("обновление без message")
                continue
        }

        log.Println(update.Message.Text)
}
```

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

- Новый текст сообщения
- Старый текст до правки
- ✅ обновление без message
- Программа упадёт с паникой

Правки приезжают отдельным типом обновления: текст лежит в поле EditedMessage, а Message у такого Update пуст. Сработает ветка nil и напечатается заглушка из лога — правки эхо-бот просто не видит.

### Соберите отправку ответа

Впишите конструктор сообщения, поле реплая и метод отправки.

```go
msg := tgbotapi.___(update.Message.Chat.ID, "готово")
msg.___ = update.Message.MessageID

if _, err := bot.___(msg); err != nil {
        log.Println(err)
}
```

Подсказка: Сообщение создаёт NewMessage, цитату включает поле ReplyToMessageID, отправляет bot.Send.

Правильные ответы в пропусках: `NewMessage`, `ReplyToMessageID`, `Send`

### Соберите сердце эхо-бота

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

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

1. `u := tgbotapi.NewUpdate(0)`
2. `updates := bot.GetUpdatesChan(u)`
3. `for update := range updates {`
4. `if update.Message == nil { continue }`
5. `msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)`
6. `bot.Send(msg)`

Эхо-бот работает, и это уже настоящий бот: живые люди пишут — он отвечает, день и ночь. Дальше углубление в механику: [пятый урок](/courses/telegram-bot/lessons/05-long-polling/) вскрывает long polling — что происходит между NewUpdate и сообщением в канале, зачем нужен offset и почему Timeout не декорация. После него — [отправка сообщений во всей полноте](/courses/telegram-bot/lessons/06-otpravka-soobscheniy/): chat_id, реплаи, копирование чужих сообщений. Полный маршрут курса — на [странице курса](/courses/telegram-bot/). Домашнее задание ниже — на запуск и правку: настоящие боты получаются у тех, кто ломает собственный код руками и читает чужие ошибки без страха.

### Карточки: анатомия эхо-бота

- **GetUpdatesChan** — Метод, запускающий long polling: горутина опрашивает getUpdates и кладёт Update в канал. Читается циклом for range.
- **update.Message** — Указатель *Message на сообщение в обновлении. Бывает nil: кнопки, правки и другие события живут в других полях Update.
- **Chat.ID** — Идентификатор чата типа int64: приватный диалог, группа, канал. Адресат каждого ответа бота.
- **NewMessage** — Конструктор MessageConfig: tgbotapi.NewMessage(chatID int64, text string). Отправку делает bot.Send.
- **ReplyToMessageID** — Поле MessageConfig: ID сообщения для цитаты-реплая. Ноль — обычное сообщение без цитаты.
- **bot.Send** — Отправка любого конфига: сообщение, фото, клавиатура. Возвращает Message и error — ошибку логируем всегда.

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

- Эхо-бот — полный конвейер: получить 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 разбирается в уроке про деплой.
