Розробка веб-серверів на Go

Alex Alex 24 вересня
Розробка веб-серверів на Go

Стандартна бібліотека мови 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(":8000", nil)
}

Якщо ви запустите цей код і відкриєте в браузері сторінку http://localhost:8000, то побачите текст «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 - це центр видачі SSL сертифікатів, який видає безкоштовні сертифікати з можливістю їх автоматичного оновлення. Для використання цього сервісу в Go-застосунках доступний пакет autocert.

Налаштувати Let's Encrypt найпростіше використовуючи допоміжний метод autocert.NewListener у зв'язці з http.Server. Допоміжний метод отримує та оновлює 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 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/v1:

// 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())

Можливість організації маршрутів і застосування просунутих правил маршрутизації спрощує структуризацію і супровід великих застосунків.

Реалізація 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
        }
        // 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)

	// Set up static file serving
	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(). Ми запустимо сервер в goroutine і будемо прослуховувати канал для прийняття сигналу переривання (наприклад комбінації CTRL+C). Після отримання сигналу виділимо кілька секунд на коректне завершення роботи сервера.

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-серверів.

Для продовження рекомендуємо подивитися статтю на Cloudflare, щоб дізнатися про попередні кроки перед тим, як розгортати HTTP-сервер на Go в Інтернеті.

Якщо вам цікаві шаблони, які ви можете використовувати у власних програмах, зайдіть в проєкт http-boilerplate на GitHub. А щоб побачити описані прийоми програмування в реальних продуктах, відвідайте проєкт Gophish на GitHub.

Джерело: getgophish.com

Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Війти / Зареєструватися