Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 10 из 25 ~16 мин +50 XP

Группы маршрутов

Заготовку api := r.Group("/api/v1") мы мельком видели во втором уроке, а в седьмом вешали на группы middleware — теперь полный разбор. В gin группы маршрутов — это не синтаксический сахар, а самостоятельный тип RouterGroup с собственным basePath, указателем на движок и цепочкой звеньев: движок сам является группой, и именно поэтому у него есть методы GET и Group. Построим дерево /api → /v1 → /admin с requireAPIKey-заглушкой на админке, разведём версии API v1 и v2 через общие хендлеры и заменим дефолтные «404 page not found» и «405 method not allowed» на свои NoRoute и NoMethod с JSON-конвертом. А [валидацию из прошлого урока](/courses/gin/lessons/09-validaciya/) заберём с собой — в группах она не меняется ни на букву.

Содержание урока · 12 разделов

gin группы маршрутов: RouterGroup вместо копипасты#

Пока приложение маленькое, пути пишут руками: r.GET("/ping"), r.POST("/todos"). Как только эндпоинов становится больше десяти, начинается ерунда: префикс /api повторяется в каждом маршруте, переименовать его — значит пройтись поиском по всему проекту, а общий для половины путей middleware приходится дублировать аргументами. В gin группы маршрутов закрывают все три проблемы разом: r.Group("/api") возвращает значение типа *gin.RouterGroup, у которого есть те же методы GET, POST, PUT, DELETE и Use, что и у движка, — только пути внутри пишутся относительно префикса группы. Мы уже трогали тему дважды: во втором уроке мелькнула заготовка api := r.Group("/api/v1") с фигурными скобками — они, как тогда говорилось, чисто визуальный контейнер, Go их игнорирует; в седьмом уроке на группу вешали requireAPIKey. Важная деталь, которую легко упустить: Group ничего не регистрирует. Вызов r.Group("/api") не добавляет в дерево маршрутов ни одной записи — это лишь заготовка со склеенным basePath и пустым списком звеньев; пока вы не вызвали api.GET(...), группе неоткуда отвечать на запросы. Поэтому группы можно создавать впрок, пустая группа безвредна — она просто не участвует в обработке, — а комментарии к каждой ветке дерева пишут прямо рядом с объявлением. Фигурные скобки после Group — по желанию: они визуально ограничивают «зону» группы в файле, компилятору на них всё равно.

Базовая группа: все задачи под /api

GO
r := gin.Default()

// Корень API: все пути ниже — относительные.
api := r.Group("/api")

api.GET("/todos", listTodos)          // GET /api/todos
api.POST("/todos", createTodo)        // POST /api/todos
api.GET("/todos/:id", getTodo)        // GET /api/todos/:id
api.DELETE("/todos/:id", deleteTodo)  // DELETE /api/todos/:id

if err := r.Run(":8080"); err != nil {
        panic(err)
}

Слева в комментариях — фактические пути, которые получит клиент. createTodo здесь — тот же хендлер из уроков про привязку и валидацию: ShouldBindJSON с TodoInput, проверка тегов, конверт ответа — ни одна строка не поменялась при переезде в группу. Единственное правило — относительные пути: /todos внутри группы с префиксом /api даёт /api/todos. Напишете /api/todos — получите /api/api/todos, и gin не предупредит: оба маршрута спокойно зарегистрируются.

Анатомия RouterGroup: basePath, движок и корень#

Откройте в исходниках файл routergroup.go: тип RouterGroup — это четыре поля. Handlers HandlersChain — список middleware, приклеенных к группе; basePath string — уже вычисленный префикс; engine Engine — указатель на движок, чтобы группа могла регистрировать маршруты в общем дереве; root bool — флаг «я корневая группа». На этом держится трюк, из-за которого у движка есть методы групп: структура Engine первым полем встраивает RouterGroup, и конструктор New() инициализирует это поле со basePath "/" и root: true. Поэтому r := gin.Default() — это gin.Engine, но r.GET и r.Group работают: методы встроенной группы поднимаются на поверхность. Создание вложенной группы — это создание новой структуры: сигнатура Group(relativePath string, handlers ...HandlerFunc) *RouterGroup, а Group() возвращает &RouterGroup{Handlers: group.combineHandlers(handlers), basePath: group.calculateAbsolutePath(relativePath), engine: group.engine} — механику склейки звеньев мы разобрали в седьмом уроке, здесь важен basePath: он вычисляется сразу, при создании, функцией joinPaths — это path.Join плюс сохранение хвостового слэша, если относительный путь кончался слэшем. path.Join чистит двойные слэши и точечные сегменты, поэтому "/api/" и "/v1" склеятся в аккуратное /api/v1. Итоговый префикс можно распечатать: метод BasePath() возвращает group.basePath — удобно для стартового лога «зарегистрированы маршруты: …» и для проверок в тестах. Сами группы — маленькие дешёвые структуры: смело заводите по одной на смысловую зону, никакой платы за лишний уровень вложенности нет.

Проверка: хвостовой слэш в префиксах

api := r.Group("/api/"); v1 := api.Group("/v1/"). Что вернёт v1.BasePath()?

Вложенные группы: /api/v1/admin за три строки#

Сила групп — в композиции. api := r.Group("/api") — корень; v1 := api.Group("/v1") — версия; admin := v1.Group("/admin", checkAPIKey("…")) — защищённая ветка. basePath каждого следующего уровня приклеивается к basePath родителя: /api, /api/v1, /api/v1/admin. Вложенные группы наследуют и звенья: маршруты admin несут всё, что объявлено на api, на v1 и в аргументах собственной Group, — общий срез склеивается в момент регистрации маршрута, поэтому порядок «звенья до маршрутов» важен, и мы проверим это предсказанием ниже. Практика больших проектов: дерево собирается в одной функции buildAPI, каждая версия — в своей функции-регистраторе, тогда границы версий видны одним взглядом, а git-история не перемешивает правки разных зон. Глубина вложенности ничем не ограничена — /api/v1/admin/reports/weekly это четыре законных уровня; но каждый уровень должен иметь право на собственный middleware или собственный смысл, иначе это просто лишний отступ. Типичная ошибка новичка — забыть про относительность: v1.GET("/api/v1/todos", …) внутри группы даст путь /api/v1/api/v1/todos. gin не ругается — двойной префикс регистрируется как обычный маршрут и молча ловит запросы. И последнее: префикс можно и «перепрыгнуть», потому что joinPaths исполняет path.Join, а тот понимает «..» — v1.Group("../admin") даст /api/admin. Работает, но читается ужасно: в продакшене так делать не стоит.

Поздний Use: кого он коснётся

Прочитайте код и выберите, что он выведет:

r := gin.Default()

api := r.Group("/api")

api.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"msg": "pong"})
})

// Режим обслуживания включили «позже»:
api.Use(func(c *gin.Context) {
        c.AbortWithStatusJSON(http.StatusServiceUnavailable,
                gin.H{"err": "maintenance"})
})

api.GET("/info", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"ver": "v1"})
})

// curl localhost:8080/api/ping
// curl localhost:8080/api/info

Middleware на группу: снимок звеньев в момент регистрации#

Как вешать звенья на группу, мы отработали в уроке про кастомные middleware: два способа — аргументы Group и вызовы g.Use, а порядок в цепочке задаёт combineHandlers, склеивающий срезы при регистрации маршрута. Здесь добавим только два следствия, которые в прошлый раз остались за кадром. Следствие первое: снимок звеньев делается в момент вызова api.GET — всё, что добавлено в группу после регистрации маршрута, на него не влияет; api.Use не пересчитывает существующие маршруты. Отсюда правило из предсказания выше: сначала Use, потом маршруты — и держите их в коде именно в таком порядке. Следствие второе: групповые звенья не действуют на 404. Запрос к несуществующему пути /api/v1/nowhere обслуживается цепочкой движка, в которой групповых звеньев нет вовсе — только глобальные и NoRoute-хендлер, о котором речь впереди. Поэтому глобальный Use из урока про встроенные middleware видит вообще всё, а групповой — только свои маршруты: requireAPIKey на группе admin не защищает «ничего» — несуществующий путь под /admin получит 404 мимо ключа, и это корректное поведение, а не дыра.

Практика: дерево /api/v1 с защищённой админкой

GO
// checkAPIKey — requireAPIKey-подобная заглушка: полная версия с c.Set — в уроке 07.
func checkAPIKey(secret string) gin.HandlerFunc {
        return func(c *gin.Context) {
                if c.GetHeader("X-API-Key") != secret {
                        // Конверт из урока про JSON-ответы.
                        c.AbortWithStatusJSON(http.StatusUnauthorized, Envelope{
                                Error: &APIError{
                                        Code:    "UNAUTHORIZED",
                                        Message: "неверный или пустой X-API-Key",
                                },
                        })
                        return
                }
                c.Next()
        }
}

// registerTodoRoutes ставит маршруты задач на ЛЮБУЮ группу:
// один и тот же набор хендлеров для v1 и v2.
func registerTodoRoutes(rg *gin.RouterGroup) {
        rg.GET("/todos", listTodos)
        rg.POST("/todos", createTodo) // внутри — ShouldBindJSON(TodoInput) из урока 09
        rg.GET("/todos/:id", getTodo)
}

func buildAPI(r *gin.Engine) {
        api := r.Group("/api")

        v1 := api.Group("/v1") // стабильная версия
        registerTodoRoutes(v1)

        admin := v1.Group("/admin", checkAPIKey("local-secret"))
        admin.GET("/stats", adminStats) // GET /api/v1/admin/stats

        v2 := api.Group("/v2") // следующая версия: слой общий, форматы свои
        registerTodoRoutes(v2)
        v2.GET("/todos/:id/history", todoHistory) // только в v2
}

Три уровня вложенности и один переиспользуемый регистратор. checkAPIKey — заглушка гварда: сравнение заголовка и AbortWithStatusJSON конвертом; для админки этого достаточно, а в уроке про JWT на это место встанет настоящая проверка токена. Заметьте: registerTodoRoutes принимает gin.RouterGroup, а не gin.Engine, — поэтому ей всё равно, куда её зовут. Хендлеры listTodos, createTodo и getTodo — те же функции, что и в прошлых уроках: группы меняют только адресацию, не логику.

Версии API: v1 и v2 на одном дереве#

Версионирование путями — самый простой и самый gin-идиоматичный способ эволюции API: префиксы v1 и v2 живут в одном дереве маршрутов, одном процессе и одном порту, а группы делают разводку бесплатной. Сценарий: формат ответа Todo поменялся — добавилось поле priority и дата в RFC 3339 вместо строки. Клиентов на старом формате ломать нельзя: v1 замораживается — в неё летят только багфиксы, — а v2 получает собственные хендлеры с новой структурой ответа. Когда логика общая, а отличается только сериализация, выручает функция-регистратор с параметром *gin.RouterGroup: registerTodoRoutes(v1) и registerTodoRoutes(v2) ставят одни и те же хендлеры под оба префикса; когда расходится сам контракт, у версии свои хендлеры, но общий сервис-слой. Чего делать не стоит: ветвиться по версии внутри одного хендлера, выдирая v2 из пути через c.Param — контракт размазывается по коду, и через год никто не скажет, что вообще умеет v1. План вывода версии тоже прописывается маршрутизацией: логируйте трафик по префиксам (BasePath в помощь), отвечайте на v1 заголовком Sunset с датой отключения из звена версии, потом отключайте — дерево маршрутов честно покажет, кто ещё ходит на старую версию. В трекере мы разводим версии так: v1 — стабильная, v2 — с историей изменений задачи; общие хендлеры зарегистрированы обеим версиям, специфичный /todos/:id/history — только второй.

Проверка: эволюция формата ответа

В v2 изменился формат ответа Todo: дата уехала в RFC 3339, добавилось поле priority. Как правильно развести версии в gin?

NoRoute и NoMethod: свои 404 и 405#

Дефолтные ответы движка на промах — тела «404 page not found» и «405 method not allowed» с Content-Type text/plain. Для JSON-API это брак: фронтенд ждёт конверт. Замена — два метода движка, и важно, что именно движка: func (engine Engine) NoRoute(handlers ...HandlerFunc) и func (engine Engine) NoMethod(handlers ...HandlerFunc) — у RouterGroup таких методов нет, r в r.NoRoute — это всегда корневой движок. Док-комментарий NoRoute честен: «It returns a 404 code by default» — хендлеры вы подставляете свои, статус остаётся 404. Первое правило: NoRoute заменяет предыдущий набор, а не дополняет — в исходниках присваивание engine.noRoute = handlers, поэтому звенья передавайте одним вызовом (r.NoRoute(jsonNotFound, extra)) или собирайте цепочку заранее; это отличие от Use, который дописывает. Второе: движок пересобирает итоговую цепочку промаха — allNoRoute = combineHandlers(noRoute) — при каждом вызове NoRoute и глобальном Use, поэтому глобальные звенья (Logger, Recovery, requestID) исполняются и на 404: строка [GIN] в логе появится и для промаха. Групповых звеньев там нет — см. прошлый блок. Третье: NoMethod бездействует, пока не включён флаг r.HandleMethodNotAllowed = true — мы упоминали его во втором уроке; без флага запрос с чужим методом валится в NoRoute как обычный 404. С флагом gin ищет путь в деревьях других методов: нашёл — отвечает 405 с заголовком Allow со списком допустимых (требование RFC 7231) и вашим хендлером; не нашёл ни в одном дереве — это уже не «не тот метод», а «нет пути», и правильный ответ — 404 через NoRoute. Дефолтные тела пишутся только если ваш хендлер не записал ничего: статус движок ставит заранее.

NoRoute и NoMethod с конвертом Envelope

GO
r := gin.Default()
r.HandleMethodNotAllowed = true // без флага NoMethod не вызовется никогда

// Все промахи мимо дерева маршрутов — JSON-конверт вместо text/plain.
r.NoRoute(func(c *gin.Context) {
        c.JSON(http.StatusNotFound, Envelope{
                Error: &APIError{
                        Code:    "NOT_FOUND",
                        Message: "маршрут не найден: " + c.Request.URL.Path,
                },
        })
})

// Путь есть, метод чужой: 405 и Allow движок поставит сам.
r.NoMethod(func(c *gin.Context) {
        c.JSON(http.StatusMethodNotAllowed, Envelope{
                Error: &APIError{
                        Code:    "METHOD_NOT_ALLOWED",
                        Message: "метод не поддержан этим маршрутом",
                },
        })
})

api := r.Group("/api")
v1 := api.Group("/v1")
registerTodoRoutes(v1)

Оба вызова — variadic: r.NoRoute(jsonNotFound, extra) соберёт цепочку из нескольких хендлеров, но повторный вызов NoRoute заменит её целиком. Статус движок выбирает до ваших хендлеров, заголовок Allow для 405 ставит маршрутизатор — хендлер пишет только тело. Если хендлер ничего не записал, gin доложит дефолтные «404 page not found» или «405 method not allowed». Эхо пути в Message помогает фронту увидеть опечатку — главное, не отдавать наружу лишних деталей.

Проверка: 404 мимо ключа

Зарегистрированы: r.Use(requestID), admin := v1.Group("/admin", checkAPIKey("…")) и r.NoRoute(jsonNotFound). Запрос GET /api/v1/admin/unknown без ключа — что получит клиент?

405 или 404: два DELETE-запроса

Прочитайте код и выберите, что он выведет:

r := gin.New()
r.HandleMethodNotAllowed = true

api := r.Group("/api")
v1 := api.Group("/v1")
v1.GET("/todos", listTodos)
v1.POST("/todos", createTodo)

r.NoMethod(func(c *gin.Context) { c.String(405, "method!") })

// curl -X DELETE localhost:8080/api/v1/todos
// curl -X DELETE localhost:8080/api/v1/unknown

Соберите скелет API

Допишите флаг 405, вложенные группы и свои обработчики промахов.

func buildAPI(r *gin.Engine) { r. = true // чужой метод у существующего пути — честный 405 api := r.Group("/api") // корень API v1 := api.("/v1") // версия внутри корня admin := v1.("/admin", checkAPIKey("local-secret")) admin.GET("/stats", adminStats) r.(notFound) // конверт NOT_FOUND на все промахи r.(methodNotAllowed) // конверт METHOD_NOT_ALLOWED }

Соберите путь 405-запроса

DELETE /api/v1/todos при включённом HandleMethodNotAllowed и своих NoRoute/NoMethod: разложите события от curl до строки лога.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

RouteGroup или RouterGroup: кто есть кто в исходниках#

Названия вокруг групп — постоянный источник путаницы, разберём раз и навсегда. Тип в исходниках называется RouterGroup — файл routergroup.go; варианта RouteGroup в gin нет, это популярная описка из статей. По-русски говорят и «группы роутов gin», и «роутер-группы» — имеется в виду один и тот же gin.RouterGroup. Отдельного типа «роутер» не существует: gin.Router не скомпилируется. Роутер в разговорной речи — это gin.Engine, который, как мы выяснили, встраивает RouterGroup со basePath "/" и root: true. Компилятор следит за этим хозяйством через два интерфейса, объявленных в начале routergroup.go. IRoutes — «что умеет любой маршрутный узел»: Use, Handle, Any, GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, Match и статические помощники Static, StaticFS, StaticFile. IRouter — это IRoutes плюс Group: право создавать подгруппы есть только у того, что может быть родителем. Строки var _ IRouter = (RouterGroup)(nil) и var _ IRouter = (Engine)(nil) — проверки компиляции: и движок, и группа обязаны удовлетворять интерфейсу. Любопытен Any: он регистрирует путь сразу на девять методов — GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE, — а Match берёт список методов срезом: rg.Match([]string{"GET", "POST"}, "/todos", h). Практический вывод для своего кода: функции-регистраторы принимайте как *gin.RouterGroup — тогда их можно звать и на v1, и на v2, а при необходимости и на корень.

Слэши, редиректы и что дальше#

Три бытовые мелочи, на которых спотыкаются. Первая: ведущий слэш в относительном пути можно не писать — joinPaths исполняет path.Join, и api.Group("v1") даст тот же /api/v1. Вторая: хвостовой слэш. RedirectTrailingSlash включён по умолчанию: если маршрут зарегистрирован как /api/v1/todos, а запрос пришёл на /api/v1/todos/, движок не даст 404, а ответит редиректом — 301 для GET, 307 для остальных методов, сохраняя метод. Удобно, но клиенты ведут себя с редиректами POST по-разному: некоторые библиотеки не follow 307 без явной настройки — лучше не полагаться и держать пути в одном виде. Третья: пути групп и маршрутов именно склеиваются — двойного слэша не будет никогда, path.Join съест и его; а вот «..» в относительном пути реально поднимает ветку выше, что мы уже отмечали как анти-паттерн. Статические файлы — соседняя тема, и её хелперы живут прямо здесь же, в интерфейсе IRoutes: Static, StaticFS, StaticFile, — но это материал урока 11-statika, там и продолжим. А пока зафиксируем скелет, который вы унесёте из урока: buildAPI собирает дерево /api → /v1 и /v2 → /admin с ключом-заглушкой; registerTodoRoutes переиспользует хендлеры между версиями; NoRoute и NoMethod отвечают конвертом; и ни одна строка бизнес-логики при переезде в группы не изменилась.

Шесть правил групп маршрутов#

  • r.Group("/api") возвращает новую *gin.RouterGroup: basePath склеен joinPaths (path.Join чистит слэши), сама Group ничего не регистрирует — маршруты ставят только GET/POST/…
  • вложенные группы наследуют префиксы и звенья: /api → /v1 → /admin; снимок цепочки делается в момент регистрации маршрута — поздний Use на зарегистрированное не действует
  • middleware на группу — аргументами Group или g.Use; групповые звенья не видят 404 и 405, глобальный Use видит всё
  • версии API — вложенные группы v1/v2: общие хендлеры переиспользует функция-регистратор с параметром *gin.RouterGroup; v1 заморожена и выводится по логу префиксов
  • NoRoute и NoMethod — методы движка, а не группы: NoRoute заменяет набор хендлеров, цепочка allNoRoute включает глобальные звенья — Logger печатает [GIN] и для промаха
  • NoMethod работает только при HandleMethodNotAllowed = true и находит путь в другом метод-дереве: 405 с заголовком Allow; если пути нет нигде — честный 404

Карточки: группы маршрутов

Термин

RouterGroup

нажмите, чтобы перевернуть
Определение

Тип из routergroup.go с четырьмя полями: Handlers (цепочка звеньев), basePath (склеенный префикс), engine (*Engine), root (флаг корня). Group() создаёт новую структуру, ничего не регистрируя; Engine встраивает RouterGroup со basePath "/" и root: true — поэтому у движка есть GET и Group.

1 / 6

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

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

  • RouterGroup — четыре поля (Handlers, basePath, engine, root): Group создаёт новую структуру со склеенным basePath и ничего не регистрирует; Engine встраивает RouterGroup со basePath "/" и root: true, поэтому у движка есть GET и Group.
  • Вложенные группы склеивают префиксы через joinPaths (path.Join плюс хвостовой слэш) и наследуют звенья; BasePath() возвращает итоговый префикс для логов и тестов.
  • Снимок звеньев делается в момент регистрации маршрута: поздний api.Use не действует на уже зарегистрированные маршруты; групповые звенья не видят 404 — в отличие от глобального Use.
  • Версии API — вложенные группы v1/v2: общие хендлеры переиспользует функция-регистратор с параметром *gin.RouterGroup, v1 заморожена и выводится по логу префиксов.
  • NoRoute и NoMethod — методы движка: NoRoute заменяет набор хендлеров и пересобирает allNoRoute, где исполняются глобальные звенья; без своего хендлера клиент получает text/plain.
  • NoMethod бездействует без HandleMethodNotAllowed = true; с флагом — 405 и заголовок Allow, а путь, неизвестный ни одному методу, честно уходит в 404.
  • Тип называется RouterGroup, а не RouteGroup; интерфейсы IRoutes и IRouter описывают маршрутный узел и родителя групп — свои функции-регистраторы принимайте *gin.RouterGroup.

Домашнее задание

Переведите трекер на группы: соберите buildAPI с корнем /api, вложите v1 и v2, вынесите регистратор todoRoutes(rg *gin.RouterGroup) и позовите его для обеих версий, а v2 дополните маршрутом /todos/:id/history. На v1.Group("/admin", checkAPIKey("…")) закройте /stats заглушкой из урока. Включите r.HandleMethodNotAllowed = true и добавьте свои NoRoute/NoMethod с конвертами NOT_FOUND и METHOD_NOT_ALLOWED; убедитесь curl-ом, что DELETE /api/v1/todos даёт 405 с Allow, GET /api/v1/nope — 404-конверт, а строка [GIN] печатается и для промаха: глобальный Logger входит в цепочку allNoRoute.

Личные заметкиПусто
Хранятся только в вашем браузере (Gin · Группы маршрутов)

Вопросы и ответы по уроку

Чем Group отличается от Use в gin?

Group создаёт новый узел дерева: basePath склеивается из префикса родителя и относительного пути, возвращается *RouterGroup, на которой регистрируют маршруты. Use дописывает звенья в цепочку существующего узла. Их часто комбинируют: v1 := api.Group("/v1", logger) — префикс и звено одним вызовом.

Почему NoRoute нельзя вызвать на группе v1?

NoRoute и NoMethod объявлены на *Engine: таблица маршрутов одна на весь движок, и «свой 404 для ветки» движок различить не умеет — цепочка промаха собирается из глобальных звеньев и noRoute-хендлеров. Если нужен разный 404 для /api и остального — один NoRoute внутри разбирает c.Request.URL.Path по префиксам.

Группы замедляют маршрутизацию?

Нет. Группа — конструкция времени старта: basePath склеен строками, звенья скопированы в срез в момент регистрации маршрута. В обработке запроса gin идёт по radix-дереву конкретного метода и исполняет готовый срез хендлеров — групп в рантайме не существует.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Echo25 уроков~13 чпрактика25 уроков уже доступны

Echo: минималистичный быстрый фреймворк

Курс Echo для Go: маршруты, middleware, валидация, JWT, SSE, тестирование и деплой. 25 уроков с примерами и итоговым API-проектом.

  • Строить REST API на Echo
  • Использовать типизированные запросы/ответы (Request/Response)
  • Управлять ошибками через echo.HTTPError
  • Писать middleware, включая JWT-аутентификацию