Пять лет назад я начал разрабатывать 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 рекомендует:
- Двухлетний практический курс «Я — веб-разработчик PRO».
- Образовательный онлайн-курс «Профессия Java-разработчик».
- Практический годовой курс «PHP-разработчик с 0 до PRO».
Комментарии (5)
Metod12
02.04.2019 19:45Было бы здорово показать, как получать параметры из пути и/или формы, желательно с валидацией.
Также хотя бы пару слов про fasthttp — раз в 10 быстрее, но с проблемами совместимости со стандартным net/http.trawl
03.04.2019 08:10Было бы здорово показать, как получать параметры из пути и/или формы, желательно с валидацией.
Но ведь на примере go-chi/chi показано, как получить параметр из пути...
func NewRouter() http.Handler { router := chi.NewRouter() r.Get("/{name}", HelloName) return r } func HelloName(w http.ResponseWriter, r *http.Request) { name := chi.URLParam(r, "name") response := HelloResponse{ Message: fmt.Sprintf("Hello %s!", name), } }
Получение параметров из формы — это уже парсинг body/query запроса, там сложностей нет.
Валидация параметров роута — в документации к роутеру (например, тот же
chi
):
// Regexp url parameters: r.Get("/{articleSlug:[a-z-]+}", getArticleBySlug)
KirEv
03.04.2019 04:42многого всякого сделал на Go, и микросервисы, и файловые бд для простых нетривиальных задач, и много чего другого…
но Go — не такой прост как кажеться, и то что описано в переводе — вредно и опасно, далеко от реальности…
А в делах повседневных сталкиваться приходиться и с бд, и с конкурентностью и с гонкой данных, которые не всегда возможно избежать (особенно новичкам), и тур в го и лучшие практики на офф.сайте не всегда дают исчерпывающий ответ на вопросы как поступить в конкретном общепринятом случае… в результате получается странное переиспользование памяти, тормозящий сборщик мусора и тупо переполнение тсп стека или к-во соединений целевой бд… (зароется один червь в большом проекте — поседеете пока отладите)
но я не об этом… низкий порог вхождения, много готовых либ, удобный синтаксис и т.п…
сегодня столкнулся с простой задачей, есть 5 функций, каждая использует переменную Х, этот Х делается глобальным в пределах сорса (выше func main объявляется как var x []byte — например), потому что — зачем сшивать этой переменной все функции.
далее — внимание, делается чтото типо:
func fZ(){ .... x, err := ioutil.ReadAll(z); ... }
и тут самый интересный конфюз… много всяких неоднозначностей и странностей описывал в остальных комментах к постам о якобы простом Go.
…
вот на прошлой недели была очень тупая и простая задача, есть 170+ баз данных в mcaccess, есть утилита которая делает из мс аксес экспорт в таблиц и данных для конкретной бд (по другому она не умеет, даже пароль не просит, крутая утилита)… я столкнулся с проблемой использования возможностей из коробки, ибо мой многопоточный скрипт генерил примерно 6гб данных примерно за 40 минут (базы и связки привда сложные и их было много, кроме того тулза для каждой бд могда экспортировать только 1 таблицу, тоесть если бд в 100мб и 10 таблиц, то 100мб проганяется 10 раз)…
психонул, ибо это не дело… написал тупой алгоритм на си, вместо 40сек получил 19сек, я не хотел разбираться почему в любимом Go что-то не работало быстро, искать узкие места и т.п., но вернулся к старому и доброму: лучше потрахаться на уровне компиляции «вычленяя» проблемы указателей, чем потом отлаживать проект в несколько тысяч строк в поисках логической ошибки… и профайлер не панацея.
это кстати больше всего бесит (на начальных парах) — можно заставить работать мапинг «неправильно», случайно, а потом он будет крашиться, но все скомпилируется…
peresada
Меня одного забавляет тот факт, что в статье про Go прорекламированы все возможные курсы, кроме самого Go?