Понимание Mux и обработчика путем написания собственного RESTful Mux

Понимание 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

}
  1. Мы создаем пустую структуру, которая реализует интерфейс http.Handler
  2. http.Handle зарегистрирует наш handler для заданного шаблона, в нашем случае это "/". Почему шаблон, а не URI? Потому что под капотом, когда ваш сервер работает и получает любой запрос, он найдет ближайший шаблон к пути запроса и отправит запрос соответствующему обработчику. Это означает, что если вы попытаетесь вызвать 'http://localhost:8000/some/other/path?value=foo ' он все равно будет отправлен нашему зарегистрированному обработчику, даже если он зарегистрирован под шаблоном "/".
  3. В последней строке с 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://ieftimov.com/ posts/make-resilient-golang-net-http-servers-using-timeouts-deadlines-context-cancellation/

https://blog.cloudflare.com/the-complete- руководство по golang-net-http-timeouts/

:::информация Также опубликовано здесь.

:::


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE