Стандартная библиотека языка Go включает в себя множество полезных и функциональных компонентов «из коробки», которые позволяют легко разрабатывать серверные приложения. В статье мы изучим, как написать веб-сервер на Go. Начнем с базового «Hello World!» и закончим приложением, выполняющим следующие функции:
- использует Let’s Encrypt для HTTPS;
- выполняет маршрутизацию запросов к API;
- реализует промежуточную обработку запросов (middleware);
- раздаёт статические файлы;
- корректно завершает свою работу.
Если вы хотите сразу увидеть готовый код, зайдите в репозиторий http-boilerplate на GitHub.
Hello World!
HTTP-сервер на 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)
}
Если вы запустите приложение и откроете в браузере страницу http://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
.
Идея представить обработчики единым интерфейсом открывает много возможностей. Позже мы увидим, что промежуточная обработка запросов будет реализована обработчиком, чей метод ServeHTTP
выполняет некоторый код, а после вызывает метод ServeHTTP
другого обработчика.
Таким образом, обработчики формируют ответы на запросы. Но как понять, какой именно обработчик нужно использовать в данный момент?
Маршрутизация запросов
Для выбора обработчика запроса в Go используется HTTP-мультиплексор. В некоторых библиотеках он называется «muxer» или «router», но суть та же. Мультиплексор выбирает обработчик на основе анализа пути запроса.
Если вам нужна продвинутая поддержка маршрутизации, следует обратиться к сторонним библиотекам. Достойные альтернативы — gorilla/mux и go-chi/chi — позволяют простым образом реализовать промежуточную обработку, настроить wildcard-маршрутизацию и решить ряд других задач. Однако важнее то, что они совместимы со стандартными HTTP-обработчиками. Таким образом, сохранится простота кода и возможность его безболезненного изменения в будущем.
Будьте внимательны при работе со сложными веб-фреймворками. Обычно они используют нестандартные решения, что усложняет интеграцию со стандартными обработчиками. Как правило, комбинации стандартной библиотеки и легковесного маршрутизатора будет достаточно для разработки большинства приложений.
Обработка запросов
Наконец, нам необходимо что-то, способное слушать входящие соединения и перенаправлять каждый запрос соответствующему обработчику. Эту задачу можно возложить на http.Server
.
Как мы увидим далее, сервер отвечает за всё, что связано с обработкой соединений. В частности, сюда относится работа по протоколу TLS. В нашем примере при вызове http.ListenAndServe
используется стандартный HTTP-сервер.
Теперь перейдём к более сложным примерам.
Работа с Let’s Encrypt
Начальная версия нашего приложения работает по протоколу HTTP, но при возможности всегда рекомендуется использовать протокол HTTPS. К счастью, в Go это не проблема.
Если у вас уже есть сертификат и закрытый ключ, просто используйте ListenAndServeTLS
, указав корректные пути к файлам сертификата и ключа:
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)
Но можно сделать лучше.
Let’s Encrypt — это удостоверяющий центр, выдающий бесплатные сертификаты с возможностью их автоматического обновления. Для использования этого сервиса в Go-приложениях доступен пакет autocert.
Настроить Let’s Encrypt проще всего используя вспомогательный метод autocert.NewListener
в связке с http.Serve
. Вспомогательный метод получает и обновляет TLS-сертификаты, в то время как HTTP-сервер занимается обработкой запросов:
http.Serve(autocert.NewListener("example.com"), nil)
Если требуется более тонкая настройка, используйте менеджер 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. Ниже описан пример настройки маршрутизации с помощью библиотеки chi.
Допустим, у нас есть файл api/v1/api.go, содержащий маршруты для нашего API:
// HelloResponse — это JSON-представление сообщения
type HelloResponse struct {
Message string `json:"message"`
}
// HelloName возвращает персонализированное JSON-сообщение
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 возвращает HTTP-обработчик, который является точкой входа в API
func NewRouter() http.Handler {
r := chi.NewRouter()
r.Get("/{name}", HelloName)
return r
}
В основном файле установим для маршрутов префикс api/v1
:
// NewRouter возвращает новый HTTP-обработчик, который является точкой входа в приложение
func NewRouter() http.Handler {
router := chi.NewRouter()
router.Mount("/api/v1/", v1.NewRouter())
return router
}
http.Serve(autocert.NewListener("example.com"), NewRouter())
Возможность организации маршрутов и применение продвинутых правил маршрутизации упрощает структуризацию и сопровождение крупных приложений.
Реализация middleware
Промежуточная обработка (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
}
// Аутентификация прошла успешно, направляем запрос следующему обработчику
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.
Корректное завершение работы
Начиная с версии 1.8 в Go есть возможность корректно завершать работу HTTP-сервера вызовом метода Shutdown()
. Мы запустим сервер в горутине и будем прослушивать канал для приёма сигнала прерывания (например комбинации CTRL+C). После получения сигнала выделим несколько секунд на корректное завершение работы сервера.
handler := server.NewRouter()
srv := &http.Server{
Handler: handler,
}
go func() {
srv.Serve(autocert.NewListener(domains...))
}()
// Ожидание сигнала
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
// Попытка корректного завершения
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)
Заключение
Стандартная библиотека языка Go невероятно функциональна. В доказательство этого были рассмотрены её встроенные возможности и преимущества гибких интерфейсов для быстрой разработки надежных HTTP-серверов.
В качестве продолжения рекомендуем посмотреть статью на Cloudflare, чтобы узнать о предварительных шагах перед тем, как разворачивать HTTP-сервер на Go в Интернете.
Если вам интересны шаблоны, которые вы можете использовать в собственных приложениях, зайдите в проект http-boilerplate на GitHub. А чтобы увидеть описанные приемы программирования в реальных продуктах, посетите проект Gophish на GitHub.
Перевод статьи «Building Web Servers in Go»
Борис Рязанцев