
Понимание Mux и обработчика путем написания собственного RESTful Mux
10 ноября 2022 г.Все примеры кода можно найти здесь.
В этой статье мы попытаемся понять две важные концепции Go — mux
и Handler
. Но прежде чем мы начнем писать какой-либо код, давайте сначала поймем, что нам нужно для запуска простого веб-сервиса.
- Прежде всего нам нужен сам сервер, который будет работать на некотором порту, прослушивать запросы и предоставлять ответы на эти запросы.
- Следующее, что нам нужно, это маршрутизатор. Этот объект отвечает за маршрутизацию запросов к соответствующим обработчикам. В мире го servermux фактически является аналогом маршрутизатора.
- Последнее, что нам нужно, это обработчик. Он отвечает за обработку запроса, выполнение бизнес-логики вашего приложения и предоставление ответа на него.
Реализация интерфейса обработчика
Давайте начнем наше путешествие с чистого кода Go, без библиотек или фреймворков. Чтобы запустить сервер, нам нужно реализовать интерфейс Handler:
type Handler interface{
ServeHTTP(ResponseWriter, *Request)
}
Для этого нам нужно создать пустую структуру и предоставить для нее метод:
package main
import (
"fmt"
"net/http"
)
type handler struct{}
func (t *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ping %sn", r.URL.Query().Get("name"))
}
func main() {
h := &handler{} //1
http.Handle("/", h) //3
http.ListenAndServe(":8000", nil) //4
}
- Мы создаем пустую структуру, которая реализует интерфейс
http.Handler
http.Handle
зарегистрирует нашhandler
для заданного шаблона, в нашем случае это "/". Почему шаблон, а не URI? Потому что под капотом, когда ваш сервер работает и получает любой запрос, он найдет ближайший шаблон к пути запроса и отправит запрос соответствующему обработчику. Это означает, что если вы попытаетесь вызвать 'http://localhost:8000/some/other/path?value=foo ' он все равно будет отправлен нашему зарегистрированному обработчику, даже если он зарегистрирован под шаблоном "/".- В последней строке с
http.ListenAndServe
мы запускаем сервер на порту 8000. Имейте в виду второй аргумент, который пока равен нулю, но мы рассмотрим его подробно через несколько минут. .
Давайте проверим, как это работает с помощью curl:
❯ curl "localhost:8000?name=foo"
ping foo
Использование функции-обработчика
Следующий пример очень похож на предыдущий, но он короче и проще в разработке. Нам не нужно реализовывать какой-либо интерфейс, просто создать функцию с сигнатурой, аналогичной Handler.ServeHTTP
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "ping %sn", r.URL.Query().Get("name"))
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8000", nil)
}
Обратите внимание, что под капотом это просто синтаксический сахар, позволяющий избежать создания экземпляров Handler для каждого шаблона. HandleFunc
— это адаптер, который преобразует его в структуру с помощью метода serveHTTP
. Итак, вместо этого:
type root struct{}
func (t *root) ServeHTTP(w http.ResponseWriter, r *http.Request) {...}
type home struct{}
func (t *home) ServeHTTP(w http.ResponseWriter, r *http.Request) {...}
type login struct{}
func (t *login) ServeHTTP(w http.ResponseWriter, r *http.Request) {...}
//...
http.Handle("/", root)
http.Handle("/home", home)
http.Handle("/login", login)
Мы можем просто использовать этот подход:
func root(w http.ResponseWriter, r *http.Request) {...}
func home(w http.ResponseWriter, r *http.Request) {...}
func login(w http.ResponseWriter, r *http.Request) {...}
...
http.HandleFunc("/", root)
http.HandleFunc("/home", home)
http.HandleFunc("/login", login)
Создание собственного ServeMux
Помните, мы передали nil
в http.ListenAndServe
? Что ж, под капотом http-пакет будет использовать ServeMux по умолчанию и привязывать к нему обработчики с помощью http.Handle
и http.HandleFunc
. В производственной среде не рекомендуется использовать serveMux по умолчанию, потому что это глобальная переменная, поэтому любой пакет может получить к ней доступ и зарегистрировать новый маршрутизатор или что-то похуже. Итак, давайте создадим наш собственный serveMux. Для этого мы будем использовать функцию http.NewServeMux
. Он возвращает экземпляр ServeMux, который также имеет методы Handle
и HandleFunc
.
mux := http.NewServeMux()
mux.HandleFunc("/", handlerFunc)
http.ListenAndServe(":8000", mux)
Интересно, что наш мультиплексор
тоже является обработчиком. Подпись http.ListenAndServe
, ожидающая обработчика в качестве второго аргумента, и после получения запроса наш HTTP-сервер вызовет метод serveHTTP
нашего мультиплексора, который, в свою очередь, вызовет serveHTTP
метод зарегистрированных обработчиков.
Поэтому необязательно предоставлять мультиплексор из http.NewServeMux()
. Чтобы понять это, давайте создадим собственный экземпляр маршрутизатора.
package custom_router
import (
"fmt"
"net/http"
)
type router struct{}
func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/foo":
fmt.Fprint(w, "here is /foo")
case "/bar":
fmt.Fprint(w, "here is /bar")
case "/baz":
fmt.Fprint(w, "here is /baz")
default:
http.Error(w, "404 Not Found", 404)
}
}
func main() {
var r router
http.ListenAndServe(":8000", &r)
}
И давайте проверим это:
❯ curl "localhost:8000/foo"
here is /foo
Имейте в виду, что servermux, предоставляемый Go, будет обрабатывать каждый запрос в отдельной горутине. Вы можете попробовать реализовать собственный маршрутизатор, работающий как мультиплексор Go, используя горутины для каждого запроса.
Создание собственного сервера
Что произойдет, если клиент позвонит в нашу конечную точку, которой нужно получить информацию из БД, но БД не отвечает в течение длительного времени? Будет ли клиент ждать ответа? Если да, то, возможно, это не очень удобный API. Таким образом, чтобы избежать этой ситуации и предоставить ответ после некоторого времени ожидания, мы можем создать экземпляр http.Server
, который имеет функцию ListenAndServe
. В производстве нам часто нужно настроить наш сервер, например. предоставить нестандартный регистратор или установить тайм-ауты.
srv := &http.Server{
Addr:":8000",
Handler: mux,
// ReadTimeout is the maximum duration for reading the entire
// request, including the body.
ReadTimeout: 5 * time.Second,
// WriteTimeout is the maximum duration before timing out
// writes of the response.
WriteTimeout: 2 * time.Second,
}
srv.ListenAndServe()
Поведение тайм-аутов может показаться неочевидным. Что произойдет, когда тайм-аут будет превышен? Будет ли запрос принудительно закрыт или на него будет дан ответ? Давайте создадим обработчик, который будет спать в течение 2 секунд, и посмотрим, как наш сервер будет вести себя с WriteTimeout в течение 1 секунды.
func handler(w http.ResponseWriter, r *http.Request) {
time.Sleep(2 * time.Second)
fmt.Fprintf(w, "ping %sn", r.URL.Query().Get("name"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", handler)
srv := &http.Server{
Addr: ":8000",
Handler: mux,
WriteTimeout: 1 * time.Second,
}
srv.ListenAndServe()
}
Теперь давайте вызовем curl
с помощью утилиты time
, чтобы измерить, сколько времени займет наш запрос.
❯ time curl "http://localhost:8000?name=foo"
curl: (52) Empty reply from server
curl "http://localhost:8000?name=foo" 0.00s user 0.01s system 0% cpu 2.022 total
Мы не видим ответа от сервера, и запрос занял 2 секунды вместо 1. Это связано с тем, что тайм-ауты — это всего лишь механизм, который ограничивает определенные действия после превышения тайм-аута. В нашем случае запись чего-либо в ответ была ограничена по прошествии 1 секунды. В конце я предоставил 2 ссылки на замечательные статьи.
И все же у нас есть открытый вопрос: как заставить наш обработчик завершить работу через какой-то промежуток времени?
Для этого мы можем просто использовать метод http
TimeoutHandler
:
func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler
Давайте перепишем наш пример с обработчиком времени ожидания. Не забудьте увеличить time.sleep и timeout по +1 сек, иначе ответа все равно не будет:
func handler(w http.ResponseWriter, r *http.Request) {
time.Sleep(3 * time.Second)
fmt.Fprintf(w, "ping %sn", r.URL.Query().Get("name"))
}
func main() {
mux := http.NewServeMux()
mux.Handle("/", http.TimeoutHandler(http.HandlerFunc(handler), time.Second * 1, "Timeout"))
srv := &http.Server{
Addr: ":8000",
Handler: mux,
WriteTimeout: 2 * time.Second,
}
srv.ListenAndServe()
}
Теперь наш завиток работает именно так, как мы и ожидали:
❯ time curl "http://localhost:8000?name=foo"
Timeoutcurl "http://localhost:8000?name=foo" 0.01s user 0.01s system 1% cpu 1.022 total
RESTful-маршрутизация
Servemux, предоставляемый Go, не имеет удобного способа поддержки HTTP-методов. Конечно, мы всегда можем добиться того же результата, используя *http.Request
, который содержит всю необходимую информацию об ответе, включая HTTP-метод:
package main
import"net/http"
func createUser(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method Not Allowed", 405)
}
w.Write([]byte("New user has been created"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/users", createUser)
http.ListenAndServe(":3000", mux)
}
Пользовательский маршрутизатор RESTful
Теперь давайте попробуем сделать что-нибудь поинтереснее. Нам нужно, чтобы наш маршрутизатор мог регистрировать обработчики с помощью метода handleFunc
, который будет принимать 3 параметра:
строка метода
строка шаблона
f func(w http.ResponseWriter, req *http.Request)
Для этого нам нужно написать небольшой кусок кода :)
Начнем с типов. Для начала нам нужен сам роутер. У него должна быть карта, в которой будет храниться зарегистрированный шаблон URL (например, /users
) и вся информация (или правила), которые мы хотим применить к нему:
type urlPattern string
type router struct {
routes map[urlPattern]routeRules
}
func New() *router {
return &router{routes: make(map[urlPattern]routeRules)}
}
Далее давайте определим, что такое routeRules. В случае REST мы хотим хранить зарегистрированные HTTP-методы и связанные с ними обработчики:
type httpMethod string
type routeRules struct {
methods map[httpMethod]http.Handler
}
Теперь мы хотим, чтобы наш маршрутизатор имел метод HandleFunc
:
/*
method - string, e.g. POST, GET, PUT
pattern - URL path for which we want to register a handler
f - handler
*/
func (r *router) HandleFunc(method httpMethod, pattern urlPattern, f func(w http.ResponseWriter, req *http.Request)) {
rules, exists := r.routes[pattern]
if !exists {
rules = routeRules{methods: make(map[httpMethod]http.Handler)}
r.routes[pattern] = rules
}
rules.methods[method] = http.HandlerFunc(f)
}
Последнее, что нам нужно, это чтобы наш маршрутизатор реализовал интерфейс Handler
. Итак, нам нужно реализовать метод ServeHTTP(w http.ResponseWriter, req *http.Request)
:
func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// first we will try to find a registered URL pattern
foundPattern, exists := r.routes[urlPattern(req.URL.Path)]
if !exists {
http.NotFound(w, req)
return
}
// next we will try to check if such HTTP method was registered
handler, exists := foundPattern.methods[httpMethod(req.Method)]
if !exists {
notAllowed(w, req, foundPattern)
return
}
// finally we will call registered handler
handler.ServeHTTP(w, req)
}
// small helper method
func notAllowed(w http.ResponseWriter, req *http.Request, r routeRules) {
methods := make([]string, 1)
for k := range r.methods {
methods = append(methods, string(k))
}
w.Header().Set("Allow", strings.Join(methods, " "))
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
И все. Теперь зарегистрируем простой обработчик:
func handler(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("hello"))
}
func main() {
r := New()
r.HandleFunc(http.MethodGet, "/test", handler)
http.ListenAndServe(":8000", r)
}
И проверьте, как это работает:
❯ curl -X GET -i "http://localhost:8000/test"
HTTP/1.1 200 OK
Date: Wed, 13 Jul 2022 14:24:43 GMT
Content-Length: 5
Content-Type: text/plain; charset=utf-8
hello
❯ curl -X POST -i "http://localhost:8000/test"
HTTP/1.1 405 Method Not Allowed
Allow: GET
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Wed, 13 Jul 2022 14:24:14 GMT
Content-Length: 19
Method Not Allowed
Отлично. Мы только что написали маршрутизатор, который может легко зарегистрировать обработчик для определенного метода HTTP. Полный код выглядит так:
package main
import (
"net/http"
"strings"
)
type httpMethod string
type urlPattern string
type routeRules struct {
methods map[httpMethod]http.Handler
}
type router struct {
routes map[urlPattern]routeRules
}
func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
foundRoute, exists := r.routes[urlPattern(req.URL.Path)]
if !exists {
http.NotFound(w, req)
return
}
handler, exists := foundRoute.methods[httpMethod(req.Method)]
if !exists {
notAllowed(w, req, foundRoute)
return
}
handler.ServeHTTP(w, req)
}
func (r *router) HandleFunc(method httpMethod, pattern urlPattern, f func(w http.ResponseWriter, req *http.Request)) {
rules, exists := r.routes[pattern]
if !exists {
rules = routeRules{methods: make(map[httpMethod]http.Handler)}
r.routes[pattern] = rules
}
rules.methods[method] = http.HandlerFunc(f)
}
func notAllowed(w http.ResponseWriter, req *http.Request, r routeRules) {
methods := make([]string, 1)
for k := range r.methods {
methods = append(methods, string(k))
}
w.Header().Set("Allow", strings.Join(methods, " "))
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
func New() *router {
return &router{routes: make(map[urlPattern]routeRules)}
}
func handler(w http.ResponseWriter, req *http.Request) {
w.Write([]byte("hello"))
}
func main() {
r := New()
r.HandleFunc(http.MethodGet, "/test", handler)
http.ListenAndServe(":8000", r)
}
Почему бы не использовать его в рабочей версии? Ну, потому что есть несколько библиотек, которые предлагают вам такие функции и многое другое! Ограничения по заголовку хоста, обработка путей с параметрами пути, параметры запроса, сопоставление с образцом (мы реализуем только точное равенство) и многое другое.
Мультиплексор Gorilla
Одной из самых популярных библиотек для этого является Gorilla/mux.
❯ go get "github.com/gorilla/mux"
Вот простой пример обработчика GET.
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/test", handler).Methods("GET")
http.ListenAndServe(":8000", r)
}
Войти в полноэкранный режим Выйти из полноэкранного режима
Давайте проверим эту конечную точку и посмотрим на результат:
❯ curl -X GET "http://localhost:8000/test"
Hello World!
Войти в полноэкранный режим Выйти из полноэкранного режима
И если мы попытаемся отправить метод POST, мы получим 405:
❯ curl -X POST -i "http://localhost:8000/test"
HTTP/1.1 405 Method Not Allowed
Date: Wed, 13 Jul 2022 12:54:22 GMT
Content-Length: 0
Войти в полноэкранный режим Выйти из полноэкранного режима
Мультиплексор Gorilla:
https://github.com/gorilla/mux
Статьи о тайм-аутах:
https://blog.cloudflare.com/the-complete- руководство по golang-net-http-timeouts/
:::информация Также опубликовано здесь.
:::
Оригинал