Перейти к основному содержимому

Первая программа на 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:

EchoGin
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

EchoGinFiber
Основаnet/httpnet/httpfasthttp
Ошибкиreturn errorявный c.JSONreturn c.Status(...).JSON(...)
Валидацияvalidator + middlewarebinding тегивручную или плагины
Встраивание в старый muxдачерез http.Handlerнужен адаптер

Частые ошибки

СимптомПричина
Валидация не срабатываетНе задан e.Validator / нет Validator() middleware
405 Method Not AllowedЗарегистрирован другой HTTP-метод
Пустой или двойной ответНет return после c.JSON
Middleware из stdlib "как есть"Нужна обёртка под сигнатуру Echo

Что попробовать

  1. e.GET("/api/notes/:id", getNote) — одна заметка по id.
  2. Middleware с замером времени вокруг next(c).
  3. Тест 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.

Мини-практикум

  1. Подключите e.Validator и проверьте validate-теги на POST /api/notes.
  2. Внедрите единый HTTPErrorHandler с одинаковым JSON-форматом ошибок.
  3. Добавьте graceful shutdown на базе http.Server.

Типичные ошибки

  • Используется c.Bind, но валидация тегов не подключена.
  • После c.JSON нет return, и handler пишет ответ повторно.
  • Доменные ошибки возвращаются как 500, хотя нужен 400/404/409.

Дальше


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.


В подборках

Статья входит в тематические подборки и блок "С чего начать?" на главной. Соседние шаги того же маршрута:

Первые шаги (маршрут подборки) — Первая программа на Gin, Первая программа на Fiber, Spring Boot на Kotlin — первая программа, Первая программа на Axum, Compose Multiplatform — первая программа, Первая программа Electron с React.