Улучшенная маршрутизация HTTP-серверов в Go 1.22
- среда, 18 октября 2023 г. в 00:00:17
В Go 1.22 ожидается появление интересного предложения - расширение возможностей по поиску шаблонов (pattern-matching) в мультиплексоре, используемом по умолчанию для обслуживания HTTP в пакете net/http
.
Существующий мультиплексор (http.ServeMux) обеспечивает рудиментарное сопоставление путей, но не более того. Это привело к появлению целой индустрии сторонних библиотек для реализации более мощных возможностей. Я рассматривал эти возможности в серии статей "REST-серверы на Go", в частях 1 и 2.
Новый мультиплексор в версии 1.22 позволит значительно сократить отставание от пакетов сторонних разработчиков, обеспечив расширенное согласование. В этой небольшой заметке я кратко расскажу о новом мультиплексоре (mux). Я также вернусь к примеру из серии "REST-серверы на Go" и сравню, как новый stdlib mux справляется с gorilla/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.
Сообщение подробное и полезное. Если мы столкнемся с конфликтами в сложных схемах регистрации (особенно когда паттерны регистрируются в нескольких местах исходного кода), то такие подробности будут очень ценны.
В серии статей "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. Расширение возможностей стандартной библиотеки - это положительный момент для всего сообщества, независимо от того, используют ли люди сторонние пакеты или ограничиваются стандартной библиотекой.