В Go 1.22 ожидается появление интересного предложения - расширение возможностей по поиску шаблонов (pattern-matching) в мультиплексоре, используемом по умолчанию для обслуживания HTTP в пакете net/http.

Существующий мультиплексор (http.ServeMux) обеспечивает рудиментарное сопоставление путей, но не более того. Это привело к появлению целой индустрии сторонних библиотек для реализации более мощных возможностей. Я рассматривал эти возможности в серии статей "REST-серверы на Go", в частях 1 и 2.

Новый мультиплексор в версии 1.22 позволит значительно сократить отставание от пакетов сторонних разработчиков, обеспечив расширенное согласование. В этой небольшой заметке я кратко расскажу о новом мультиплексоре (mux). Я также вернусь к примеру из серии "REST-серверы на Go" и сравню, как новый stdlib mux справляется с gorilla/mux.

Использование нового mux

Если вы когда-либо использовали сторонние пакеты mux/маршрутизаторов для Go (например, gorilla/mux), то использование нового стандартного mux будет простым и привычным. Начните с чтения документации по нему - она краткая и понятная.

Давайте рассмотрим несколько базовых примеров использования. Наш первый пример демонстрирует некоторые из новых возможностей mux по сопоставлению шаблонов:

package main

import (
	"fmt"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /path/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprint(w, "got path\n")
	})

	mux.HandleFunc("/task/{id}/", func(w http.ResponseWriter, r *http.Request) {
		id := r.PathValue("id")
		fmt.Fprintf(w, "handling task with id=%v\n", id)
	})

	http.ListenAndServe("localhost:8090", mux)
}

Опытные программисты на Go сразу же заметят две новые особенности:

  • В первом обработчике метод HTTP (в данном случае GET) указывается явно в составе шаблона. Это означает, что данный обработчик сработает только для GET-запросов к путям, начинающимся с /path/, а не для других HTTP-методов.

  • Во втором обработчике во втором компоненте пути присутствует подстановочный знак - {id}, который ранее не поддерживался. Подстановочный знак будет соответствовать одному компоненту пути, и обработчик сможет получить доступ к найденному значению через метод PathValue запроса.

Поскольку Go 1.22 еще не выпущен, я рекомендую запускать этот пример с gotip. Полный пример кода с инструкциями по его выполнению. Давайте проверим работу этого сервера:

$ gotip run sample.go

А в отдельном терминале мы можем выполнить несколько вызовов curl для проверки:

$ curl localhost:8090/what/
404 page not found

$ curl localhost:8090/path/
got path

$ curl -X POST localhost:8090/path/
Method Not Allowed

$ curl localhost:8090/task/f0cd2e/
handling task with id=f0cd2e

Обратите внимание, что сервер отклоняет POST-запрос к /path/, в то время как GET-запрос (по умолчанию для curl) разрешен. Обратите также внимание на то, как подстановочный знак id получает значение при совпадении запроса. Еще раз рекомендую вам ознакомиться с документацией по новому ServeMux. Вы узнаете о таких дополнительных возможностях, как сопоставление путей с подстановочным символом {id}..., строгое сопоставление конца пути с {$} и другие правила.

Особое внимание в предложении было уделено возможным конфликтам между различными шаблонами. Рассмотрим такую схему:

mux := http.NewServeMux()
mux.HandleFunc("/task/{id}/status/", func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")
        fmt.Fprintf(w, "handling task status with id=%v\n", id)
})
mux.HandleFunc("/task/0/{action}/", func(w http.ResponseWriter, r *http.Request) {
        action := r.PathValue("action")
        fmt.Fprintf(w, "handling task 0 with action=%v\n", action)
})

А если сервер получит запрос на /task/0/status/ - к какому обработчику он должен обратиться? Он соответствует обоим! Поэтому в новой документации по ServeMux тщательно описаны правила старшинства для шаблонов, а также возможные конфликты. В случае конфликта регистрация впадает в панику. Действительно, для приведенного выше примера мы получаем что-то вроде:

panic: pattern "/task/0/{action}/" (registered at sample-conflict.go:14) conflicts with pattern "/task/{id}/status/" (registered at sample-conflict.go:10):
/task/0/{action}/ and /task/{id}/status/ both match some paths, like "/task/0/status/".
But neither is more specific than the other.
/task/0/{action}/ matches "/task/0/action/", but /task/{id}/status/ doesn't.
/task/{id}/status/ matches "/task/id/status/", but /task/0/{action}/ doesn't.

Сообщение подробное и полезное. Если мы столкнемся с конфликтами в сложных схемах регистрации (особенно когда паттерны регистрируются в нескольких местах исходного кода), то такие подробности будут очень ценны.

Переделка моего сервера задач с новым mux

В серии статей "REST-серверы в Go" реализуется простой сервер для приложения задач/списка задач на Go, используя несколько различных подходов. Часть 1 начинается с "ванильного" подхода с использованием стандартной библиотеки, а часть 2 переделывает тот же сервер с использованием маршрутизатора gorilla/mux.

Сейчас самое время реализовать его еще раз, но уже с использованием улучшенного mux из Go 1.22; особенно интересно будет сравнить решение с использованием gorilla/mux.

Полный код этого проекта доступен здесь. Рассмотрим несколько показательных примеров кода, начав с регистрации паттерна:

mux := http.NewServeMux()
server := NewTaskServer()

mux.HandleFunc("POST /task/", server.createTaskHandler)
mux.HandleFunc("GET /task/", server.getAllTasksHandler)
mux.HandleFunc("DELETE /task/", server.deleteAllTasksHandler)
mux.HandleFunc("GET /task/{id}/", server.getTaskHandler)
mux.HandleFunc("DELETE /task/{id}/", server.deleteTaskHandler)
mux.HandleFunc("GET /tag/{tag}/", server.tagHandler)
mux.HandleFunc("GET /due/{year}/{month}/{day}/", server.dueHandler)

Вы, наверное, заметили, что эти шаблоны не очень строги к частям пути, которые идут после интересующей нас части (например, /task/22/foobar). Это соответствует остальной части серии, но новый http.ServeMux позволяет легко ограничить пути с помощью подстановочного символа {$}, если это необходимо.

Как и в примере gorilla/mux, здесь мы используем специфические HTTP-методы для маршрутизации запросов (с одинаковым путем) к разным обработчикам; в старой версии http.ServeMux такие матчеры должны были обращаться к одному и тому же обработчику, который в зависимости от метода решал, что делать.

Рассмотрим также один из обработчиков:

func (ts *taskServer) getTaskHandler(w http.ResponseWriter, req *http.Request) {
  log.Printf("handling get task at %s\n", req.URL.Path)

  id, err := strconv.Atoi(req.PathValue("id"))
  if err != nil {
    http.Error(w, "invalid id", http.StatusBadRequest)
    return
  }

  task, err := ts.store.GetTask(id)
  if err != nil {
    http.Error(w, err.Error(), http.StatusNotFound)
    return
  }

  renderJSON(w, task)
}

Он извлекает значение ID из req.PathValue("id"), аналогично подходу Gorilla, однако, поскольку у нас нет regexp, указывающего, что {id} соответствует только целым числам, нам приходится обращать внимание на ошибки, возвращаемые strconv.Atoi.

В целом, конечный результат очень похож на решение с использованием gorilla/mux из второй части. Обработчики разделены гораздо лучше, чем в ванильном stdlib-подходе, поскольку теперь mux может выполнять более сложную маршрутизацию, не оставляя многие решения по маршрутизации на усмотрение самих обработчиков.

Заключение

Вопрос "Какой пакет маршрутизаторов мне использовать?" всегда был FAQ для начинающих Go-программистов. Я полагаю, что после выхода Go 1.22 общие ответы на этот вопрос изменятся, поскольку многие сочтут новый stdlib mux достаточным для своих нужд, не прибегая к использованию пакетов сторонних разработчиков.

Другие будут придерживаться привычных пакетов сторонних разработчиков, и это совершенно нормально. Такие маршрутизаторы, как gorilla/mux, по-прежнему предоставляют больше возможностей, чем стандартная библиотека; кроме того, многие Go-программисты предпочитают использовать легкие фреймворки, такие как Gin, которые предоставляют не только маршрутизатор, но и дополнительные инструменты для создания веб-бэкендов.

В целом, это, безусловно, положительное изменение для всех пользователей Go. Расширение возможностей стандартной библиотеки - это положительный момент для всего сообщества, независимо от того, используют ли люди сторонние пакеты или ограничиваются стандартной библиотекой.

Комментарии (11)


  1. vkomp
    17.10.2023 11:43
    +2

    Меня и раньше устраивал net/http. А теперь еще и модняво... ;)
    Хорошо, что можно метод сразу указать. Да и id'шку приспособим, хотя не вспомню чтобы мучился.


  1. pocoZ
    17.10.2023 11:43
    +4

    Не совсем понимаю мотивацию разработчиков. Да, круто, что добавляют новое, но тратить свое золотое время на настолько некритичную вещь, которая давно отлично решается пакетами созданными сообществом это выглядит странно.


    1. RC_Cat
      17.10.2023 11:43

      Наерное для того, чтобы не нужно было использовать сторонние библиотеки.


  1. slovacccc
    17.10.2023 11:43

    Выглядит как костыль, могли бы добавить просто новый пакет, где уже можно было бы передавать не только pattern, а method, pattern, как это сделано во многих роутерах.


    1. QtRoS
      17.10.2023 11:43

      Мне тоже показалось странноватым решение, которое подразумевает парсинг (хоть и тривиальный). Можно принести свои сомнения в PR: у Go довольно открытое коммьюнити, а опыт участия в таких дискуссиях и для личного развития, и для резюме полезен.


    1. Nurked
      17.10.2023 11:43
      +1

      В общем я с вами соглашусь. Но тут вот в чём момент. У Го есть такая ниша - это именно серверное программирование. И прикол в том, что го с этой задачей справляется хорошо даже без зависимостей. Да чёрт, перед программой на го даже nginx не нужен. Сертификаты, раутинг, мьюксинг, и хорошая параллельная обработка - то что нужно. Плюс у них вообще всё по стандартам. Нет никаких линтеров, вопросов по поводу того как ставить скобочки и как обрабатывать ошибки.

      Вот они ещё одну необходимую фитчу перевели в ряд "из коробки". Ещё бы JWT добавили с TOML, тогда я бы вообще без щависимостей писал.

      А то я вас могу пригласить в мир нода. Недавно пришлось обновить какой-то пакет, он за собой потянул TS5, вместо 4, и мне пол-дня потребовалось на переписывание всего. А вот с этим под крылом можно не париться по поводу обновлений гориллы.


      1. stvoid
        17.10.2023 11:43

        Понимаю что решение не универсальное, но по поводу конфигов, мы давно забили на всякие toml, yaml и берём обычный json.

        Благо отформатировать есть всегда в чем, всем знаком, стандартные библиотеки для паркинга есть во всех популярных ЯП.

        По поводу этого апдейта очень рад. Потому что горилла не столько библиотека, сколько просто декоратор расширяющий стандартную либу.

        ИМХО, из реально имеющих смысл библиотек/фреймворков есть только 2 - стандартная, либо с батарейками гориллы и fasthttp.


      1. micronull
        17.10.2023 11:43
        +3

        Нет никаких линтеров

        https://golangci-lint.run/

        Отсутствие линтеров скорее минус, чем плюс.

        Хорошо что Go предоставляет богатые возможности для написания своих линтеров.


  1. anaxita
    17.10.2023 11:43
    +3

    Классно, делать пет проекты и в целом небольшие сервисы без нагрузки теперь можно без зависимости от роутера. Мне кажется классно, что берут подходы из популярных кастомных решений и делают их из коробки. Сначала slog, теперь роутер, Красота!


  1. chechyotka
    17.10.2023 11:43
    +2

    Как по мне, прокидывать в одну строку и метод, и путь -- не самое лучшее решение


    1. comerc Автор
      17.10.2023 11:43

      Почему?