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

Автор оригинала: Jordan Wright
  • Перевод


Пять лет назад я начал разрабатывать 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 рекомендует:

  • +24
  • 12,9k
  • 5
Skillbox
145,03
Онлайн-университет профессий будущего
Поделиться публикацией

Комментарии 5

    +13
    Меня одного забавляет тот факт, что в статье про Go прорекламированы все возможные курсы, кроме самого Go?
      0
      Было бы здорово показать, как получать параметры из пути и/или формы, желательно с валидацией.

      Также хотя бы пару слов про fasthttp — раз в 10 быстрее, но с проблемами совместимости со стандартным net/http.
        +1
        Было бы здорово показать, как получать параметры из пути и/или формы, желательно с валидацией.

        Но ведь на примере 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)
          0
          Да, Вы абсолютно правы, спасибо!
        –5
        многого всякого сделал на 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 что-то не работало быстро, искать узкие места и т.п., но вернулся к старому и доброму: лучше потрахаться на уровне компиляции «вычленяя» проблемы указателей, чем потом отлаживать проект в несколько тысяч строк в поисках логической ошибки… и профайлер не панацея.

        это кстати больше всего бесит (на начальных парах) — можно заставить работать мапинг «неправильно», случайно, а потом он будет крашиться, но все скомпилируется…

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое