Розробка веб-серверів на Golang - від простого до складного

Розробка веб-серверів на Golang - від простого до складного

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

Ця стаття присвячена написанню сервера на Go. Почнемо з простих речей, на кшталт Hello world!, а закінчимо додатком з такими можливостями:

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

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

Нагадуємо: для всіх читачів "Хабра" - знижка 10 000 рублів при записі на будь-який курс Skillbox за промокодом "Хабр".

Привіт Світ!

Створити веб-сервер 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)
}

Після цього, якщо запустити програму та відкрити сторінку локальний, то ви відразу побачите текст 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, але все це те саме. Функція мультиплексора полягає в аналізі шляху запиту та виборі відповідного оброблювача.

Якщо потрібна підтримка складної маршрутизації, тоді краще скористатися сторонніми бібліотеками. Одні з найбільш просунутих горила/мукс и 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)

Завжди можна зробити краще.

Давай шифрувати дає безкоштовні сертифікати з можливістю автоматичного поновлення. Для того, щоб скористатися сервісом, потрібний пакет 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-маршрути або процедуру встановлення шаблонів і параметрів шляхів.

У цьому випадку варто використовувати пакети горила/мукс и 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

Додати коментар або відгук