Написание сервера с помощью пакета http. Понимание концепции Mux и Handler путем написания собственного RESTful Mux.

Все примеры кода можно найти здесь.

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

  • Прежде всего, нам нужен сам сервер, который будет работать на некотором порту, прослушивать запросы и предоставлять ответы на них.
  • Следующее, что нам нужно, — это маршрутизатор. Этот элемент отвечает за маршрутизацию запросов к соответствующим обработчикам. В мире Go servemux фактически является аналогом маршрутизатора.
  • Последнее, что нам нужно, — это обработчик. Он отвечает за обработку запроса, выполнение бизнес-логики вашего приложения и предоставление ответа на него.

Реализация интерфейса обработчика

Давайте начнем наше путешествие с чистого кода 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

}
Enter fullscreen mode Выйти из полноэкранного режима
  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 — это адаптер, который преобразует его в struct с методом 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)
Вход в полноэкранный режим Выход из полноэкранного режима

Интересно то, что наш mux тоже является обработчиком. Сигнатура http.ListenAndServe ждет обработчик в качестве второго аргумента и после получения запроса наш HTTP сервер вызовет serveHTTP метод нашего mux, который в свою очередь вызовет serveHTTP метод зарегистрированных обработчиков.

Таким образом, не обязательно предоставлять mux из 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
Войти в полноэкранный режим Выход из полноэкранного режима

Имейте в виду, что servemux, предоставляемый Go, будет обрабатывать каждый запрос в отдельной goroutine. Вы можете попробовать реализовать собственный маршрутизатор, действующий как Go mux, используя goroutines для каждого запроса.

Создание собственного сервера

Что произойдет, если клиент обратится к нашей конечной точке, которой нужно получить информацию из БД, но БД долго не отвечает? Будет ли клиент ждать ответа? Если да, то, вероятно, это не очень удобный 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()
}
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь наш curl работает именно так, как мы ожидали:

❯ 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 параметра:

  • method string,
  • pattern string,
  • 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)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Почему бы не использовать это в prod? Ну, потому что есть несколько библиотек, которые предлагают вам такие возможности и многое другое! Ограничения по заголовку хоста, обработка путей с параметрами пути, параметры запроса, сопоставление шаблонов (мы реализуем только точное равенство) и многое другое.

Gorilla mux

Одна из самых популярных библиотек для этого — 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 mux:

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-guide-to-golang-net-http-timeouts/

Оцените статью
devanswers.ru
Добавить комментарий