[Перевод] Разработка REST-серверов на Go. Часть 2: применение маршрутизатора gorilla/mux
Там мы говорили об одной проблеме нашего сервера, которая заключается в том, что логика маршрутизации разбросана по нескольким местам нашей программы.
Это — проблема, с которой сталкиваются все, кто пишет HTTP-сервера, не используя зависимости. Если только сервер, принимая во внимание систему его маршрутов, не является до крайности минималистичной конструкцией (например — это некоторые специализированные серверы, имеющие лишь один-два маршрута), то оказывается, что размеры и сложность организации кода маршрутизатора — это нечто такое, на что очень быстро обращают внимание опытные программисты.
▍ Улучшенная система маршрутизации
Первой мыслью, которая может прийти в голову того, кто решил улучшить наш сервер, может стать идея об абстрагировании системы его маршрутизации, возможно — с использованием набора функций или типа данных с методами. Есть много интересных подходов к решению этой задачи, применимых в каждой конкретной ситуации. В экосистеме Go существует множество мощных и успешно используемых в различных проектах библиотек сторонних разработчиков, реализующих возможности маршрутизатора. Я настоятельно рекомендую взглянуть на этот материал, где сравниваются несколько подходов к обработке простых наборов маршрутов.
Перед переходом к практическому примеру вспомним о том, как устроен API нашего сервера:
POST /task/ : создаёт задачу и возвращает её ID
GET /task/ : возвращает одну задачу по её ID
GET /task/ : возвращает все задачи
DELETE /task/ : удаляет задачу по ID
GET /tag/ : возвращает список задач с заданным тегом
GET /due/// : возвращает список задач, запланированных на указанную дату
Для того чтобы сделать систему маршрутизации удобнее, мы можем поступить так:
- Можно создать механизм, позволяющий задавать отдельные обработчики для разных методов одного и того же маршрута. Например — запрос
POST /task/
должен обрабатываться одним обработчиком, а запросGET /task/
— другим. - Можно сделать так, чтобы обработчик маршрута выбирался бы на основе более глубокого, чем сейчас, анализа запросов. То есть, например, у нас при таком подходе должна быть возможность указать, что один обработчик обрабатывает запрос к
/task/
, а другой обработчик обрабатывает запрос к/task/
с числовымID
. - При этом система обработки маршрутов должна просто извлекать числовой
ID
из/task/
и передавать его обработчику каким-нибудь удобным для нас способом.
Написание собственного маршрутизатора на Go — это очень просто. Это так из-за того, что организовывать работу с HTTP-обработчиками можно, используя компоновку. Но тут я не стану потакать своему желанию написать всё самому. Вместо этого предлагаю поговорить о том, как организовать систему маршрутизации с использованием одного из самых популярных маршрутизаторов, который называется gorilla/mux.
▍ Сервер приложения для управления задачами, использующий gorilla/mux
Пакет gorilla/mux представляет собой один из самых старых и самых популярных HTTP-маршрутизаторов для Go. Слово «mux», в соответствии с документацией к пакету, расшифровывается как «HTTP request multiplexer» («Мультиплексор HTTP-запросов») (такое же значение «mux» имеет и в стандартной библиотеке).
Так как это — пакет, нацеленный на решение единственной узкоспециализированной задачи, пользоваться им очень просто. Вариант нашего сервера, в котором для маршрутизации используется gorilla/mux, можно найти здесь. Вот код определения маршрутов:
router := mux.NewRouter()
router.StrictSlash(true)
server := NewTaskServer()
router.HandleFunc("/task/", server.createTaskHandler).Methods("POST")
router.HandleFunc("/task/", server.getAllTasksHandler).Methods("GET")
router.HandleFunc("/task/", server.deleteAllTasksHandler).Methods("DELETE")
router.HandleFunc("/task/{id:[0-9]+}/", server.getTaskHandler).Methods("GET")
router.HandleFunc("/task/{id:[0-9]+}/", server.deleteTaskHandler).Methods("DELETE")
router.HandleFunc("/tag/{tag}/", server.tagHandler).Methods("GET")
router.HandleFunc("/due/{year:[0-9]+}/{month:[0-9]+}/{day:[0-9]+}/", server.dueHandler).Methods("GET")
Обратите внимание на то, что одни только эти определения тут же закрывают первые два пункта вышеприведённого списка задач, которые надо решить для повышения удобства работы с маршрутами. Благодаря тому, что в описании маршрутов используются вызовы
Methods
, мы можем с лёгкостью назначать в одном маршруте разные методы для разных обработчиков. Поиск совпадений с шаблонами (с использованием регулярных выражений) в путях позволяет нам легко различать /task/
и /task/
на самом верхнем уровне описания маршрутов.Для того чтобы разобраться с задачей, которая имеется в третьем пункте нашего списка, посмотрим на использование getTaskHandler
:
func (ts *taskServer) getTaskHandler(w http.ResponseWriter, req *http.Request) {
log.Printf("handling get task at %s\n", req.URL.Path)
// Тут и в других местах мы не проверяем ошибку Atoi, так как маршрутизатор
// принимает лишь данные, проверенные регулярным выражением [0-9]+.
id, _ := strconv.Atoi(mux.Vars(req)["id"])
ts.Lock()
task, err := ts.store.GetTask(id)
ts.Unlock()
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
renderJSON(w, task)
}
В определении маршрутов маршрут
/task/{id:[0-9]+}/
описывает регулярное выражение, используемое для разбора пути и назначает идентификатор «переменной» id
. К этой «переменной» можно обратиться, вызвав функцию mux.Vars
с передачей ей req
(эту переменную gorilla/mux хранит в контексте каждого запроса, а mux.Vars
представляет собой удобную вспомогательную функцию для работы с ней).▍ Сравнение различных подходов к организации маршрутизации
Вот как выглядит последовательность чтения кода, применяемая в исходном варианте сервера тем, кто хочет разобраться в том, как обрабатывается маршрут
GET /task/
.А вот что нужно прочитать тому, кто хочет понять код, в котором применяется gorilla/mux:
При использовании gorilla/mux придётся не только меньше «прыгать» по тексту программы. Тут, кроме того, читать придётся гораздо меньший объём кода. По моему скромному мнению это очень хорошо с точки зрения улучшения читабельности кода. Описание путей при использовании gorilla/mux — это простая задача, при решении которой нужно написать лишь небольшой объём кода. И тому, кто читает этот код, сразу понятно то, как этот код работает. Ещё одно преимущество такого подхода заключается в том, что все маршруты можно увидеть буквально раз взглянув на код, расположенный в одном месте. И, на самом деле, код настройки маршрутов выглядит теперь очень похожим на описание нашего REST API, выполненное в произвольной форме.
Мне нравится пользоваться такими пакетами, как gorilla/mux, из-за того, что подобные пакеты представляют собой узкоспециализированные инструменты. Они решают одну единственную задачу и решают её хорошо. Они не «забираются» в каждый уголок программного кода проекта, а значит, их, при необходимости можно легко убрать или заменить чем-то другим. Если вы посмотрите полный код того варианта сервера, о котором мы говорим в этой статье, то сможете увидеть, что область использования механизмов gorilla/mux ограничена несколькими строками кода. Если, по мере развития проекта, в пакете gorilla/mux будет обнаружено какое-то ограничение, несовместимое с особенностями этого проекта, задача замены gorilla/mux на другой маршрутизатор стороннего разработчика (или на собственный маршрутизатор) должна решаться достаточно быстро и просто.
Какой маршрутизатор вы использовали бы при разработке REST-сервера на Go?