Разработка веб-серверов на Golang — от простого к сложному
Пять лет назад я начал разрабатывать Gophish, это дало возможность изучить Golang. Я понял, что Go — мощный язык, возможности которого дополняются множеством библиотек. Go универсален: в частности, с его помощью можно без проблем разрабатывать серверные приложения.
Эта статья посвящена написанию сервера на Go. Начнем с простых вещей, вроде «Hello world!», а закончим приложением с такими возможностями:
— Использование Let’s Encrypt для HTTPS.
— Работа в качестве API-маршрутизатора.
— Работа с middleware.
— Обработка статических файлов.
— Корректное завершение работы.
После этого, если запустить приложение и открыть страницу 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 с указанием правильных файлов сертификата и ключа.
Let’s Encrypt дает бесплатные сертификаты с возможностью автоматического обновления. Для того, чтобы воспользоваться сервисом, нужен пакет autocert.
Самый простой способ его настроить — воспользоваться методом autocert.NewListener в комбинации с http.Serve. Метод позволяет получать и обновлять TLS-сертификаты, в то время как HTTP-сервер обрабатывает запросы:
Если мы откроем в браузере example.com, то получим HTTPS-ответ «Hello, world!».
Если нужна более тщательная настройка, то стоит воспользоваться менеджером autocert.Manager. Затем создаем собственный инстанс http.Server (до настоящего момента мы использовали его по умолчанию) и добавить менеджер в сервер TLSConfig:
Это простой способ реализации полной поддержки 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. Она возвращает обработчик, который раздает файлы из определенной директории.
Обязательно стоит помнить, что 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-серверы.