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

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

Пет години назад започнах да разработвам 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.ListenAndServe се използва стандартният 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 (досега използвахме стандартния) и добавяме мениджъра в конфигурацията на TLS на сървъра:

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 за маршрутите в основния файл.

Можем след това да го монтираме на основния рутер под префикса 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())

Леснотата на работа със сложни маршрути в 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
        }
        \/\/ Предполагайки, че аутентикацията е преминала, стартираме оригиналния обработчик
        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. В такъв случай, за да се предотврати компрометирането на директорията, е необходимо да се използва пакет неиндексиран.

Коректно приключване на работата

В Go съществува и функция за коректно приключване на работа на HTTP сървъра. Това може да стане с метода Shutdown(). Сървърът стартира в горутина, а след това каналът се прослушва за получаване на сигнал за прекъсване. След като сигналът бъде получен, сървърът се изключва, но не веднага, а след няколко секунди.

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 сървъри.

Skillbox препоръчва:

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster