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

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

Пет години назад започнах да разработвам Gophish, което ми даде възможност да се запозная с Golang. Осъзнах, че Go е мощен език, чийто възможности се допълват от множество библиотеки. Go е универсален: по-специално, с него безпроблемно могат да се разработват сървърни приложения.

Тази статия е посветена на написването на сървър на Go. Да започнем с простите неща, като „Hello world!“, а да завършим с приложение с следните възможности:

— Използване на Let’s Encrypt за HTTPS.
— Работa като 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 отговор «Здравей, свят!».

Ако е необходима по-подробна настройка, трябва да се използва мениджърът 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 за маршрутите в основния файл.

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