Разработка веб-серверов на Golang — от простого к сложному

Разработка веб-серверов на Golang — от простого к сложному

Пять лет назад я начал разрабатывать Gophish, это дало возможность изучить Golang. Я понял, что Go — мощный язык, возможности которого дополняются множеством библиотек. Go универсален: в частности, с его помощью можно без проблем разрабатывать серверные приложения.

Эта статья посвящена написанию сервера на Go. Начнем с простых вещей, вроде «Hello world!», а закончим приложением с такими возможностями:

— Использование Let’s Encrypt для HTTPS.
— Работа в качестве API-маршрутизатора.
— Работа с middleware.
— Обработка статических файлов.
— Корректное завершение работы.

Skillbox рекомендует: Практический курс «Python-разработчик с нуля».

Напоминаем: для всех читателей «Хабра» — скидка 10 000 рублей при записи на любой курс Skillbox по промокоду «Хабр».

Hello, world!

Создать веб-сервер на Go можно очень быстро. Вот пример использования обработчика, который возвращает обещанный выше «Hello, world!».

package main
 
import (
"fmt"
"net/http"
)
 
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
})
http.ListenAndServe(":80", nil)
}

После этого, если запустить приложение и открыть страницу localhost, то вы сразу увидите текст «Hello, world!» (конечно, если все работает правильно).

Далее мы будем неоднократно использовать обработчик, но сначала давайте поймем, как все устроено.

net/http

В примере использовался пакет net/http, это основное средство в Go для разработки как серверов, так и HTTP-клиентов. Для того, чтобы понять код, давайте разберемся в значении трех важных элементов: http.Handler, http.ServeMux и http.Server.

HTTP-обработчики

Когда мы получаем запрос, обработчик анализирует его и формирует ответ. Обработчики в Go реализованы следующим образом:

type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

В первом примере используется вспомогательная функция http.HandleFunc. Она оборачивает другую функцию, которая, в свою очередь, принимает http.ResponseWriter и http.Request в ServeHTTP.

Другими словами, обработчики в Golang представлены единым интерфейсом, что дает множество возможностей для программиста. Так, например, middleware реализовано при помощи обработчика, где ServeHTTP сначала что-то делает, а затем вызывает метод ServeHTTP другого обработчика.

Как и говорилось выше, обработчики просто формируют ответы на запросы. Но какой именно обработчик нужно использовать в конкретный момент времени?

Маршрутизация запросов

Для того, чтобы сделать правильный выбор, воспользуйтесь HTTP-мультиплексором. В ряде библиотек его называют muxer или router, но все это одно и то же. Функция мультиплексора заключается в анализе пути запроса и выборе соответствующего обработчика.

Если же нужна поддержка сложной маршрутизации, тогда лучше воспользоваться сторонними библиотеками. Одни из наиболее продвинутых — gorilla/mux и go-chi/chi, эти библиотеки дают возможность реализовать промежуточную обработку без особых проблем. С их помощью можно настроить wildcard-маршрутизацию и выполнить ряд других задач. Их плюс — совместимость со стандартными HTTP-обработчиками. В результате вы можете написать простой код с возможностью его модификации в будущем.

Работа со сложными фреймворками в обычной ситуации потребует не совсем стандартных решений, а это значительно усложняет использование дефолтных обработчиков. Для создания подавляющего большинства приложений хватит комбинации библиотеки по умолчанию и несложного маршрутизатора.

Обработка запросов

Кроме того, нам необходим компонент, который будет «слушать» входящие соединения и перенаправлять все запросы правильному обработчику. С этой задачей без труда справится http.Server.

Ниже показано, что сервер отвечает за все задачи, которые имеют отношение к обработке соединений. Это, например, работа по протоколу TLS. Для реализации вызова http.ListenAndServer используется стандартный HTTP-сервер.

Теперь давайте рассмотрим более сложные примеры.

Добавление Let’s Encrypt

По умолчанию наше приложение работает по HTTP-протоколу, однако рекомендуется использовать протокол HTTPS. В Go это можно сделать без проблем. Если вы получили сертификат и закрытый ключ, тогда достаточно прописать ListenAndServeTLS с указанием правильных файлов сертификата и ключа.

http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)

Всегда можно сделать лучше.

Let’s Encrypt дает бесплатные сертификаты с возможностью автоматического обновления. Для того, чтобы воспользоваться сервисом, нужен пакет autocert.

Самый простой способ его настроить — воспользоваться методом autocert.NewListener в комбинации с http.Serve. Метод позволяет получать и обновлять TLS-сертификаты, в то время как HTTP-сервер обрабатывает запросы:

http.Serve(autocert.NewListener("example.com"), nil)

Если мы откроем в браузере example.com, то получим HTTPS-ответ «Hello, world!».

Если нужна более тщательная настройка, то стоит воспользоваться менеджером autocert.Manager. Затем создаем собственный инстанс http.Server (до настоящего момента мы использовали его по умолчанию) и добавить менеджер в сервер TLSConfig:

m := &autocert.Manager{
Cache:      autocert.DirCache("golang-autocert"),
Prompt:     autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.org", "www.example.org"),
}
server := &http.Server{
    Addr:      ":443",
    TLSConfig: m.TLSConfig(),
}
server.ListenAndServeTLS("", "")

Это простой способ реализации полной поддержки HTTPS с автоматическим обновлением сертификата.

Добавление нестандартных маршрутов

Дефолтный маршрутизатор, включенный в стандартную библиотеку, хорош, но он очень простой. В большинстве приложений нужна более сложная маршрутизация, включая вложенные и wildcard-маршруты или же процедуру установки шаблонов и параметров путей.

В этом случае стоит использовать пакеты gorilla/mux и go-chi/chi. С последним мы и научимся работать — ниже показан пример.

Дано — файл api/v1/api.go, содержащий маршруты для нашего API:

/ HelloResponse is the JSON representation for a customized message
type HelloResponse struct {
Message string `json:"message"`
}
 
// HelloName returns a personalized JSON message
func HelloName(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
response := HelloResponse{
Message: fmt.Sprintf("Hello %s!", name),
}
jsonResponse(w, response, http.StatusOK)
}
 
// NewRouter returns an HTTP handler that implements the routes for the API
func NewRouter() http.Handler {
r := chi.NewRouter()
r.Get("/{name}", HelloName)
return r
}

Устанавливаем для маршрутов префикс api/vq в основном файле.

We can then mount this to our main router under the api/v1/ prefix back in our main application:

// NewRouter returns a new HTTP handler that implements the main server routes
func NewRouter() http.Handler {
router := chi.NewRouter()
    router.Mount("/api/v1/", v1.NewRouter())
    return router
}
http.Serve(autocert.NewListener("example.com"), NewRouter())

Простота работы со сложными маршрутами в Go делает возможным упростить структуризацию с обслуживанием больших комплексных приложений.

Работа с middleware

В случае промежуточной обработки используется оборачивание одного HTTP-обработчика другим, что делает возможным быстро проводить аутентификацию, сжатие, журналирование и некоторые другие функции.

В качестве примера рассмотрим интерфейс http.Handler, с его помощью напишем обработчик с аутентификацией пользователей сервиса.

func RequireAuthentication(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !isAuthenticated(r) {
            http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
            return
        }
        // Assuming authentication passed, run the original handler
        next.ServeHTTP(w, r)
    })
}

Есть сторонние маршрутизаторы, например, chi, которые позволяют расширить функциональность промежуточной обработки.

Работа со статическими файлами

В стандартную библиотеку Go входят возможности для работы со статическим контентом, включая изображения, а также файлы JavaScript и CSS. Доступ к ним можно получить через функцию http.FileServer. Она возвращает обработчик, который раздает файлы из определенной директории.

func NewRouter() http.Handler {
    router := chi.NewRouter()
    r.Get("/{name}", HelloName)
 
// Настройка раздачи статических файлов
staticPath, _ := filepath.Abs("../../static/")
fs := http.FileServer(http.Dir(staticPath))
    router.Handle("/*", fs)
    
    return r

Обязательно стоит помнить, что http.Dir выводит содержимое каталога в том случае, если в нем нет основного файла index.html. В этом случае, чтобы не допустить компрометации каталога, стоит использовать пакет unindexed.

Корректное завершение работы

В Go есть и такая функция, как корректное завершение работы HTTP-сервера. Это можно сделать при помощи метода Shutdown(). Сервер запускается в горутине, а далее канал прослушивается для приема сигнала прерывания. Как только сигнал получен, сервер отключается, но не сразу, а через несколько секунд.

handler := server.NewRouter()
srv := &http.Server{
    Handler: handler,
}
 
go func() {
srv.Serve(autocert.NewListener(domains...))
}()
 
// Wait for an interrupt
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
 
// Attempt a graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)

В качестве заключения

Go — мощный язык с практически универсальной стандартной библиотекой. Ее дефолтные возможности весьма широки, а усилить их можно при помощи интерфейсов — это позволяет разрабатывать действительно надежные HTTP-серверы.

Skillbox рекомендует:

Источник: habr.com