Первая программа на Echo
Первая программа на Echo
Где применяют Echo
Echo (пакет github.com/labstack/echo/v4) — HTTP-фреймворк поверх стандартного net/http. Главное отличие от Fiber: Echo остаётся в мире http.Handler, поэтому его проще встроить в существующий Go-сервер, прокси или тесты через httptest.
Стиль API ближе к идиоматичному Go: обработчик возвращает error, а фреймворк превращает ошибку в HTTP-ответ. Ошибки уровня "404 / 400" оформляют через echo.NewHTTPError.
Тот же учебный сценарий "Заметки", что в Gin — удобно сравнить синтаксис контекста и обработку ошибок.
Обзор: Фреймворки и библиотеки Go · Fiber · Тестирование.
Что получится
REST API — /health, группа /api с заметками, порт 8080. Хранение в памяти, sync.Mutex для потокобезопасности.
Термины
| Термин | Простыми словами |
|---|---|
echo.Context | Запрос и ответ: Bind, JSON, Param, NoContent |
return err | Идиоматичный Go: ошибка = ответ клиенту |
echo.NewHTTPError | Готовая HTTP-ошибка с кодом и телом |
c.Bind | Разбор JSON (и query) в struct |
validate:"..." | Теги для валидатора (нужен middleware) |
http.Handler | Интерфейс "обработай HTTP"; Echo его реализует |
Инициализация
mkdir echo-notes && cd echo-notes
go mod init example.com/echo-notes
go get github.com/labstack/echo/v4
Разбор:
- Первая команда создаёт папку проекта и переключает текущую директорию на неё.
go mod initсоздаёт файлgo.mod, который фиксирует имя модуля и основу dependency graph.go get github.com/labstack/echo/v4загружает Echo v4 и записывает зависимость в модуль.- После этих шагов проект готов к запуску
go run .и дальнейшему добавлению исходников.
REST API "Заметки"
main.go:
Код ITЗагрузка примера кода…
Разбор:
echo.New()создаёт экземпляр приложения и центральный роутер.e.Use(middleware.Logger())иe.Use(middleware.Recover())подключают базовые middleware для логов и восстановления после паники.e.GET("/health", ...)регистрирует health endpoint и возвращает JSON черезreturn c.JSON(...).e.Group("/api")помогает централизованно назначить префикс для набора related маршрутов.e.Logger.Fatal(e.Start(":8080"))запускает сервер и логирует фатальную ошибку при аварийном завершении.
Две структуры — ответ и создание
Note— то, что отдаём клиенту (id+text).NoteCreate— только поле из POST; валидация наvalidate-тегах.
В минимальном примере ниже валидация по тегам сработает полностью после подключения middleware.Validator(); без него Bind всё равно заполнит struct из JSON, а проверку длины можно добавить вручную.
Handlers
Код ITЗагрузка примера кода…
Разбор:
- Все handlers в Echo возвращают
error; это делает поток управления предсказуемым и единообразным. c.Bind(&body)заполняет структуру из входного JSON, а при невалидном формате возвращает ошибку клиента.echo.NewHTTPError(...)формирует HTTP-ошибки с конкретным кодом (400,404) и сообщением.- Для изменения общего состояния используется
mu.Lock()/mu.Unlock(), чтобы избежать data race. - Удаление элемента из слайса реализовано идиоматично через
append(notes[:i], notes[i+1:]...). c.NoContent(http.StatusNoContent)возвращает204без тела, что корректно для успешногоDELETE.
Разбор отличий от Gin:
| Echo | Gin |
|---|---|
return c.JSON(...) | c.JSON(...); return |
return echo.NewHTTPError(400, "...") | c.JSON(400, gin.H{"error": ...}) |
c.Bind(&body) | c.ShouldBindJSON(&body) |
Теги validate:"required" | Теги binding:"required" |
Каждый return завершает handler — забытый return после c.JSON в Echo приведёт к двойной записи ответа (типичная ошибка новичка).
go run .
Разбор:
- Команда собирает и запускает текущий модуль за один шаг.
- Сборка идёт из директории с
go.mod, поэтому запуск обычно выполняют из корня проекта. - Если код не компилируется,
go runсразу подсветит ошибочный файл и строку. - При успешном старте сервер слушает указанный порт до принудительной остановки.
Проверка
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/api/notes
curl -X POST http://127.0.0.1:8080/api/notes \
-H "Content-Type: application/json" \
-d '{"text":"Изучить Echo"}'
curl -X DELETE http://127.0.0.1:8080/api/notes/1
Разбор:
GET /healthпроверяет, что процесс запущен и роутинг работает.GET /api/notesподтверждает чтение коллекции и базовую JSON-сериализацию.POST /api/notesсоздаёт новый объект, а заголовокContent-Typeуказывает формат тела.DELETE /api/notes/1валидирует маршрут с параметром и сценарий удаления по идентификатору.- Такой набор запросов закрывает минимальный smoke-test API без отдельного тестового фреймворка.
Валидация (опционально, но полезно)
Чтобы теги validate:"required,min=1" реально работали:
Код ITЗагрузка примера кода…
Разбор:
validator/v10подключает механизм валидации структур по тегамvalidate:"...".CustomValidatorоборачивает экземпляр валидатора в тип, который понимает Echo.- Метод
Validate(i interface{}) errorвызывается Echo автоматически при проверке входных данных. e.Validator = &CustomValidator{validator.New()}регистрирует валидатор на уровне приложения.e.Use(middleware.Validator())добавляет middleware, которое реально запускает проверку и возвращает ошибку клиенту при нарушении правил.
Без e.Validator Bind прочитает JSON, но не проверит ограничения из тегов.
Middleware и встраивание
Echo реализует http.Handler. Можно отдать его стандартному серверу:
srv := &http.Server{Addr: ":8080", Handler: e}
srv.ListenAndServe()
Разбор:
- Создаётся стандартный
http.Server, гдеHandler— сам экземпляр Echo. - Такой способ удобен, когда нужно контролировать таймауты, TLS и graceful shutdown стандартными средствами.
- Вызов
ListenAndServe()блокирует текущую горутину до ошибки или остановки процесса. - Этот пример подчёркивает совместимость Echo со стандартной библиотекой
net/http.
Кастомный middleware в Echo — функция func(next echo.HandlerFunc) echo.HandlerFunc, внутри вызывают next(c) до или после логики (замер времени, проверка заголовка).
Сравнение с Gin и Fiber
| Echo | Gin | Fiber | |
|---|---|---|---|
| Основа | net/http | net/http | fasthttp |
| Ошибки | return error | явный c.JSON | return c.Status(...).JSON(...) |
| Валидация | validator + middleware | binding теги | вручную или плагины |
| Встраивание в старый mux | да | через http.Handler | нужен адаптер |
Частые ошибки
| Симптом | Причина |
|---|---|
| Валидация не срабатывает | Не задан e.Validator / нет Validator() middleware |
405 Method Not Allowed | Зарегистрирован другой HTTP-метод |
| Пустой или двойной ответ | Нет return после c.JSON |
| Middleware из stdlib "как есть" | Нужна обёртка под сигнатуру Echo |
Что попробовать
e.GET("/api/notes/:id", getNote)— одна заметка по id.- Middleware с замером времени вокруг
next(c). - Тест handler через
e.ServeHTTP(rec, req)— см. Тестирование в Go.
Практические улучшения для Echo
Централизованный обработчик ошибок
В Echo удобно настроить единый HTTPErrorHandler, чтобы все ошибки возвращались в одном формате и с одинаковым логированием:
Код ITЗагрузка примера кода…
Разбор:
- Переопределение
e.HTTPErrorHandlerзадаёт единый глобальный формат ошибок для всех handlers. - Начальное значение
500иinternal errorвыступает безопасным fallback для неизвестных исключений. errors.As(err, &he)проверяет, является ли ошибка типом*echo.HTTPError, и извлекает код/сообщение.c.JSON(code, map[string]any{...})возвращает унифицированное тело, удобное для фронтенда и лог-аналитики.- Централизация убирает дублирование в handlers и делает обработку ошибок согласованной по всему API.
Такой слой особенно полезен, когда handlers становятся длиннее и ошибок больше.
Graceful shutdown на http.Server
e.Start(":8080") подходит для старта. Для управляемой остановки лучше использовать http.Server и сигнал завершения процесса:
srv := &http.Server{Addr: ":8080", Handler: e}
go func() { _ = srv.ListenAndServe() }()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
Разбор:
- Сервер запускается в отдельной горутине, чтобы основной поток мог ожидать сигнал остановки.
signal.NotifyContext(...)создаёт контекст, который закрывается приCtrl+CилиSIGTERM.- Блок
<-ctx.Done()синхронизирует shutdown с событием завершения процесса. context.WithTimeout(..., 5*time.Second)ограничивает время мягкой остановки активных запросов.srv.Shutdown(shutdownCtx)завершает сервис контролируемо и без резкого обрыва клиентских операций.
Валидация запроса и доменная ошибка
Полезно разделять:
- ошибки формата запроса (
400); - ошибки предметной области (
409,422); - внутренние ошибки (
500).
Это делает API предсказуемым для клиентов и уменьшает путаницу в логах.
Echo в архитектуре проекта
Echo хорошо работает как HTTP-слой вокруг сервиса/репозитория:
handlerчитает и валидирует вход;serviceпринимает решение и возвращает доменную ошибку;repositoryработает с БД.
Этот подход сохраняет код тестируемым и упрощает замену Echo на другой транспорт при необходимости.
Дополнительные сниппеты с разбором
Пример — middleware проверки заголовка
func requireRequestID(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
rid := c.Request().Header.Get("X-Request-ID")
if rid == "" {
return echo.NewHTTPError(http.StatusBadRequest, "X-Request-ID is required")
}
c.Response().Header().Set("X-Request-ID", rid)
return next(c)
}
}
// e.Use(requireRequestID)
Разбор:
- Функция принимает
next echo.HandlerFuncи возвращает новый handler, что соответствует сигнатуре middleware в Echo. c.Request().Header.Get(...)читает заголовок из входящего HTTP-запроса.- Если заголовок отсутствует, middleware возвращает
echo.NewHTTPError(400, ...)и завершает запрос. c.Response().Header().Set(...)прокидываетrequest idобратно клиенту, упрощая трассировку.return next(c)передаёт управление следующему middleware или конечному handler.- Подключение через
e.Use(requireRequestID)делает правило обязательным для всех маршрутов.
Пример — query-параметр с защитой от ошибок ввода
func parseLimit(c echo.Context) (int, error) {
raw := c.QueryParam("limit")
if raw == "" {
return 20, nil
}
v, err := strconv.Atoi(raw)
if err != nil || v <= 0 || v > 100 {
return 0, echo.NewHTTPError(http.StatusBadRequest, "limit must be in range 1..100")
}
return v, nil
}
Разбор:
QueryParam("limit")возвращает строку из URL, поэтому требуется явная конвертация в число.- При пустом значении функция задаёт дефолт
20, чтобы endpoint имел предсказуемое поведение. strconv.Atoiловит нечисловой ввод, а дополнительная проверка ограничивает допустимый диапазон.- Возврат
echo.NewHTTPErrorсразу несёт и причину, и корректный HTTP-код для клиента. - Вынесение логики в helper делает handlers короче и повышает переиспользуемость.
- Такой код проще тестировать table-driven тестами без поднятия полного сервера.
Ключевые тезисы
- Echo остаётся совместимым с
net/httpи хорошо встраивается в существующий стек. - Возврат
errorиз handler делает обработку ошибок предсказуемой и идиоматичной. - Централизованный
HTTPErrorHandlerи валидатор повышают стабильность API.
Мини-практикум
- Подключите
e.Validatorи проверьтеvalidate-теги наPOST /api/notes. - Внедрите единый
HTTPErrorHandlerс одинаковым JSON-форматом ошибок. - Добавьте graceful shutdown на базе
http.Server.
Типичные ошибки
- Используется
c.Bind, но валидация тегов не подключена. - После
c.JSONнетreturn, и handler пишет ответ повторно. - Доменные ошибки возвращаются как
500, хотя нужен400/404/409.
Дальше
- Fiber — другой стек, выше RPS в синтетических бенчмарках;
- Первая программа на Go — основы модуля и синтаксиса.
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.
В подборках
Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:
Первые шаги (маршрут подборки) — Первая программа на Gin, Первая программа на Fiber, Spring Boot на Kotlin — первая программа, Первая программа на Axum, Compose Multiplatform — первая программа, Первая программа Electron с React.