Клієнт-серверна архітектура

Лекція 11. Архітектура та маршрутизація фреймворку Gin

Знайомство з вебфреймворком Gin: архітектура, двигун Engine, контекст запиту, маршрутизація та middleware

🎯 Мета лекції

  • Познайомитися з архітектурою вебфреймворку Gin та зрозуміти його переваги над стандартним net/http.
  • Опанувати роботу з двигуном gin.Engine та деревом маршрутизації HttpRouter.
  • Навчитися використовувати центральну сутність gin.Context для роботи з запитами та відповідями.
  • Зрозуміти механізм групування маршрутів та створення власних Middleware у Gin.

🔑 Ключові терміни

  • Gin Engine: центральний об'єкт фреймворку, що керує маршрутизацією та обробкою запитів.
  • gin.Context: контекст запиту, що містить всі дані про HTTP-запит та методи для формування відповіді.
  • RouterGroup: механізм логічного групування маршрутів із спільними префіксами та middleware.
  • HttpRouter: високопродуктивний алгоритм дерева маршрутизації на основі Radix Tree.

Що таке Gin і навіщо він потрібен

На попередніх лекціях ми працювали зі стандартним пакетом net/http та маршрутизатором Chi. Ці інструменти дають повний контроль над процесом обробки запитів, але вимагають багато рутинного коду: парсинг параметрів, валідація даних, серіалізація JSON, обробка помилок.

Фреймворк Gin (Gin Web Framework) — це високопродуктивний мінімалістичний вебфреймворк для Go, який надає зручні абстракції для швидкої розробки REST API, зберігаючи при цьому швидкість та контроль на рівні стандартної бібліотеки.

Філософія Gin

Gin побудований на трьох принципах:

  1. Швидкість: використовує найшвидший роутер HttpRouter (до 40 разів швидше за стандартний http.ServeMux), пул об'єктів gin.Context для мінімізації алокацій пам'яті.
  2. Мінімалізм: надає лише необхідні інструменти, не нав'язує структуру проєкту. Ви можете застосовувати Clean Architecture так само, як із Chi.
  3. Зручність: автоматичне зв'язування (Model Binding), валідація через теги, вбудовані рендерери (JSON, XML, ProtoBuf), middleware з коробки.
Gin НЕ є каркасом (framework) у класичному розумінні (як Ruby on Rails або Django). Це швидше бібліотека-роутер з корисними утилітами, що залишає свободу архітектурних рішень розробнику.
Актуальна версія та вимоги:У 2026 році актуальною версією є Gin v1.12.0. Фреймворк вимагає Go версії 1.24 або вище.Встановлення:
go get -u github.com/gin-gonic/gin
Ключові нововведення Gin 1.12.0:
  • Покращена підтримка Protocol Buffers у content negotiation
  • Нові методи контексту: GetError(), GetErrorSlice(), Delete()
  • Підтримка encoding.UnmarshalText у URI/Query binding
  • Покращена обробка множинних значень у заголовках X-Forwarded-For
  • Оптимізація продуктивності та виправлення витоків ресурсів

Gin vs net/http vs Chi: порівняння підходів

Розглянемо простий приклад: отримання користувача за ID.

http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
    // Ручний парсинг ID із URL
    path := strings.TrimPrefix(r.URL.Path, "/users/")
    id := strings.TrimSuffix(path, "/")
    
    if id == "" {
        w.WriteHeader(http.StatusBadRequest)
        json.NewEncoder(w).Encode(map[string]string{"error": "missing user ID"})
        return
    }
    
    // Виклик бізнес-логіки
    user, err := userService.GetByID(r.Context(), id)
    if err != nil {
        w.WriteHeader(http.StatusNotFound)
        json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
        return
    }
    
    // Серіалізація відповіді
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(user)
})
Зверніть увагу на скорочення коду у Gin: не потрібно встановлювати заголовки вручну, метод c.JSON() одночасно встановлює статус-код і серіалізує відповідь.

Архітектура фреймворку Gin

Перш ніж писати код, важливо зрозуміти внутрішню будову Gin та взаємодію його компонентів.

Основні компоненти

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

package "HTTP-клієнт" #F1F5F9 {
    actor Client
}

package "Gin Framework" #DBEAFE {
    component "gin.Engine" as Engine #BFDBFE {
        [RouterGroup]
        [Middleware Chain]
        [HttpRouter Tree]
    }
    
    component "sync.Pool<gin.Context>" as Pool #E0F2FE
    
    component "Handler" as Handler #DCFCE7
}

Client --> Engine : HTTP Request
Engine --> Pool : Get Context
Pool --> Engine : Reused Context
Engine --> [Middleware Chain] : Process
[Middleware Chain] --> Handler : c.Next()
Handler --> Client : HTTP Response
Engine --> Pool : Put Context back

@enduml
  1. gin.Engine — головний об'єкт фреймворку, що реалізує інтерфейс http.Handler:
    • Управляє деревом маршрутів (роутер HttpRouter).
    • Тримає глобальний ланцюжок middleware.
    • Керує пулом об'єктів gin.Context для економії пам'яті.
  2. gin.Context — контекст конкретного HTTP-запиту:
    • Обгортка над http.Request та http.ResponseWriter.
    • Надає зручні методи: c.Param(), c.Query(), c.JSON(), c.Bind().
    • Зберігає проміжні дані між middleware через c.Set() / c.Get().
  3. RouterGroup — логічна група маршрутів із спільним префіксом та middleware:
    • Дозволяє організовувати API версії (/api/v1, /api/v2).
    • Застосовує middleware лише до підгруп маршрутів (наприклад, авторизація тільки для /admin).
  4. sync.Pool — внутрішній пул об'єктів Context:
    • Після обробки запиту об'єкт gin.Context очищається та повертається в пул замість знищення.
    • Це значно зменшує навантаження на збирач сміття (Garbage Collector).
Фреймворк Gin повністю сумісний зі стандартним http.Handler. Ви можете використовувати звичайні middleware від Chi або сторонніх бібліотек через адаптер gin.WrapH().

Життєвий цикл обробки запиту в Gin

Коли HTTP-запит надходить на сервер Gin, відбувається наступна послідовність подій:

Крок 1. Отримання Context із пулу

Gin витягує об'єкт *gin.Context із sync.Pool (або створює новий, якщо пул порожній).

Крок 2. Пошук маршруту в дереві HttpRouter

Двигун Engine виконує пошук обробника за допомогою алгоритму Radix Tree. Якщо маршрут знайдено, з нього витягуються параметри шляху (/users/:id → id=123).

Крок 3. Виконання глобальних middleware

Запускається ланцюжок middleware, зареєстрованих на рівні engine або RouterGroup. Кожен middleware може:

  • Модифікувати запит (c.Request).
  • Записати дані в контекст (c.Set()).
  • Перервати обробку (c.Abort()).
  • Передати керування далі (c.Next()).

Крок 4. Виконання handler функції

Якщо жоден middleware не перервав ланцюжок, викликається фінальний обробник маршруту, який формує відповідь (c.JSON(), c.String(), c.HTML()).

Крок 5. Повернення Context у пул

Після відправки відповіді клієнту Gin очищає gin.Context та повертає його в sync.Pool для повторного використання.

Небезпека асинхронного використання gin.Context:
Об'єкт gin.Context живе лише протягом обробки одного запиту. Якщо ви передаєте c у горутину, яка виконується після завершення handler, Context може бути вже перезаписаний іншим запитом! Використовуйте c.Copy() для створення безпечної копії.

Встановлення Gin та перший сервер

Створимо найпростіший Gin-сервер для розуміння базової роботи фреймворку.

Встановлення залежностей

# Ініціалізація нового проєкту
mkdir gin-demo
cd gin-demo
go mod init gin-demo

# Встановлення Gin
go get -u github.com/gin-gonic/gin

Мінімальний HTTP-сервер на Gin

Створимо файл main.go:

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    // Створюємо Engine з вбудованими middleware (Logger + Recovery)
    r := gin.Default()

    // Реєструємо простий GET-маршрут
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "pong",
        })
    })

    // Запускаємо сервер на порту 8080
    r.Run(":8080")
}

Запуск та тестування

go run main.go
$ go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] GET /ping --> main.main.func1 (3 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
[GIN-debug] Listening and serving HTTP on :8080
▊

Тепер надішлемо запит:

curl http://localhost:8080/ping

Очікувана відповідь:

{
  "message": "pong"
}

У консолі сервера з'явиться лог:

Лог запиту
[GIN] 2026/09/01 - 15:42:13 | 200 | 127.0.0.1 | GET /ping
Режим DEBUG vs RELEASE:
За замовчуванням Gin працює в режимі debug, який виводить детальні логи маршрутів. У продакшені встановлюйте змінну оточення GIN_MODE=release для відключення діагностичних повідомлень.

gin.Engine: двигун фреймворку

gin.Engine — це серце Gin. Він відповідає за маршрутизацію, middleware та налаштування сервера.

Способи створення Engine

Gin надає два конструктори:

// 1. gin.Default() — з вбудованими Logger та Recovery middleware
r := gin.Default()

// 2. gin.New() — чистий Engine без middleware
r := gin.New()
r.Use(gin.Logger())    // Додаємо логування вручну
r.Use(gin.Recovery())  // Додаємо panic recovery вручну
Рекомендація:
Для навчання та швидких прототипів використовуйте gin.Default(). Для продакшн-систем краще gin.New() із явним контролем middleware.

Налаштування Engine

gin.Engine має кілька корисних методів конфігурації:

r := gin.New()

// Максимальний розмір тіла запиту (за замовчуванням 32 MB)
r.MaxMultipartMemory = 8 << 20 // 8 MB

// Увімкнення автоматичного редиректу /users/ → /users
r.RedirectTrailingSlash = true

// Автоматичне виправлення регістру шляху (USERS → users)
r.RedirectFixedPath = true

// Обробка OPTIONS запитів для CORS
r.HandleMethodNotAllowed = true

// Кастомна функція обробки 404
r.NoRoute(func(c *gin.Context) {
    c.JSON(http.StatusNotFound, gin.H{"error": "route not found"})
})

// Кастомна функція обробки 405 (Method Not Allowed)
r.NoMethod(func(c *gin.Context) {
    c.JSON(http.StatusMethodNotAllowed, gin.H{"error": "method not allowed"})
})

Запуск сервера з кастомними налаштуваннями

Метод r.Run() — це зручне скорочення, але для продакшн-систем краще використовувати http.Server із таймаутами:

package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    
    r.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })

    // Створюємо http.Server з таймаутами
    srv := &http.Server{
        Addr:           ":8080",
        Handler:        r,
        ReadTimeout:    10 * time.Second,
        WriteTimeout:   10 * time.Second,
        MaxHeaderBytes: 1 << 20, // 1 MB
    }

    // Запускаємо сервер в окремій горутині
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server failed: %v", err)
        }
    }()

    log.Println("Server started on :8080")

    // Graceful Shutdown: чекаємо сигналу завершення
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    log.Println("Shutting down server...")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }

    log.Println("Server stopped gracefully")
}
Цей підхід із Graceful Shutdown дозволяє серверу коректно завершити обробку всіх активних запитів перед зупинкою, що критично важливо для продакшн-систем.

gin.Context: робота з запитами та відповідями

Об'єкт gin.Context — це центральна сутність фреймворку. Він інкапсулює всю інформацію про HTTP-запит та надає зручні методи для формування відповіді.

Анатомія gin.Context

type Context struct {
    Request  *http.Request      // Оригінальний HTTP-запит
    Writer   ResponseWriter     // Обгортка над http.ResponseWriter
    Params   Params             // Параметри шляху (/users/:id)
    Keys     map[string]any     // Контекстні дані між middleware
    Errors   errorMsgs          // Накопичені помилки
    // ... внутрішні поля
}
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
skinparam classAttributeIconSize 0

class Context {
    .. Читання параметрів запиту ..
    + Param(key: string): string
    + Query(key: string): string
    + DefaultQuery(key, default: string): string
    + PostForm(key: string): string
    + GetHeader(key: string): string
    
    .. Зв'язування даних (Binding) ..
    + ShouldBindJSON(obj: any): error
    + ShouldBindQuery(obj: any): error
    + ShouldBindUri(obj: any): error
    + ShouldBind(obj: any): error
    
    .. Формування відповіді ..
    + JSON(code: int, obj: any)
    + String(code: int, format: string, values: ...any)
    + Data(code: int, contentType: string, data: []byte)
    + Status(code: int)
    + Redirect(code: int, location: string)
    
    .. Контекстні дані ..
    + Set(key: string, value: any)
    + Get(key: string): (any, bool)
    + MustGet(key: string): any
    + Delete(key: string)
    + GetError(): error
    + GetErrorSlice(): []error
    
    .. Керування потоком ..
    + Next()
    + Abort()
    + AbortWithStatus(code: int)
    + AbortWithStatusJSON(code: int, obj: any)
}

@enduml

Читання параметрів маршруту (Path Parameters)

Параметри, визначені в маршруті через :name або *name, доступні через метод c.Param():

r := gin.Default()

// Параметр :id обов'язковий
r.GET("/users/:id", func(c *gin.Context) {
    userID := c.Param("id")
    c.JSON(http.StatusOK, gin.H{
        "user_id": userID,
    })
})

// Параметр *filepath захоплює весь залишок шляху
r.GET("/files/*filepath", func(c *gin.Context) {
    path := c.Param("filepath")
    c.String(http.StatusOK, "Requested file: %s", path)
})

Приклади запитів:

GET /users/42        → user_id = "42"
GET /files/docs/readme.md → filepath = "/docs/readme.md"
Метод c.Param() завжди повертає рядок. Якщо потрібне ціле число, виконайте конвертацію через strconv.Atoi() або використовуйте Model Binding (про це в наступній лекції).

Читання query-параметрів (Query String)

Query-параметри (?key=value&foo=bar) доступні через методи c.Query() та c.DefaultQuery():

r.GET("/search", func(c *gin.Context) {
    // Обов'язковий параметр (порожній рядок, якщо відсутній)
    query := c.Query("q")
    
    // Параметр зі значенням за замовчуванням
    page := c.DefaultQuery("page", "1")
    
    // Параметр з перевіркою наявності
    if sort, exists := c.GetQuery("sort"); exists {
        c.String(http.StatusOK, "Query: %s, Page: %s, Sort: %s", query, page, sort)
    } else {
        c.String(http.StatusOK, "Query: %s, Page: %s", query, page)
    }
})

Приклади запитів:

GET /search?q=golang             → q="golang", page="1"
GET /search?q=gin&page=3         → q="gin", page="3"
GET /search?q=api&sort=date      → q="api", sort="date"

Читання даних з форм (POST Form Data)

Для форм із Content-Type: application/x-www-form-urlencoded або multipart/form-data:

r.POST("/login", func(c *gin.Context) {
    username := c.PostForm("username")
    password := c.DefaultPostForm("password", "guest")
    
    c.JSON(http.StatusOK, gin.H{
        "username": username,
        "password": password,
    })
})

Читання HTTP-заголовків

r.GET("/headers", func(c *gin.Context) {
    userAgent := c.GetHeader("User-Agent")
    authToken := c.GetHeader("Authorization")
    
    c.JSON(http.StatusOK, gin.H{
        "user_agent": userAgent,
        "auth_token": authToken,
    })
})

Формування JSON-відповідей

Найчастіший спосіб відповіді — JSON:

type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Email    string `json:"email"`
}

r.GET("/users/:id", func(c *gin.Context) {
    user := User{
        ID:       123,
        Username: "john_doe",
        Email:    "john@example.com",
    }
    
    c.JSON(http.StatusOK, user)
})

Gin автоматично:

  • Встановлює заголовок Content-Type: application/json.
  • Серіалізує структуру через encoding/json.
  • Записує статус-код.

Інші типи відповідей

r.GET("/text", func(c *gin.Context) {
    c.String(http.StatusOK, "Hello, %s!", "World")
})

Альтернативні формати серіалізації

Gin підтримує не лише JSON, але й інші формати відповідей:

XML-рендеринг:

type Person struct {
    XMLName xml.Name `xml:"person"`
    Name    string   `xml:"name"`
    Age     int      `xml:"age"`
}

r.GET("/xml", func(c *gin.Context) {
    person := Person{Name: "John", Age: 30}
    c.XML(http.StatusOK, person)
})

// Відповідь:
// <?xml version="1.0" encoding="UTF-8"?>
// <person><name>John</name><age>30</age></person>

ProtoBuf (Protocol Buffers):

Для бінарної серіалізації (потребує згенерованих структур через protoc):

r.GET("/protobuf", func(c *gin.Context) {
    user := &pb.User{Id: 1, Name: "Alice"}
    c.ProtoBuf(http.StatusOK, user)
})
ProtoBuf значно швидший за JSON (до 10 разів) та займає менше місця, але потребує спільної .proto схеми між клієнтом і сервером. Детальніше про ProtoBuf у лекції 15 (gRPC).

PureJSON — JSON без екранування HTML:

За замовчуванням c.JSON() екранує символи <, >, & для безпеки HTML. Якщо потрібен "чистий" JSON:

r.GET("/pure-json", func(c *gin.Context) {
    c.PureJSON(http.StatusOK, gin.H{
        "html": "<b>Bold text</b>",
        "url":  "https://example.com?param=value&other=123",
    })
})

// Звичайний JSON:  {"html":"\u003cb\u003eBold text\u003c/b\u003e", ...}
// PureJSON:        {"html":"<b>Bold text</b>", ...}
Використовуйте PureJSON лише для API, що віддають дані не для вбудовування в HTML (наприклад, мобільні додатки чи мікросервіси).

Контекстні дані (c.Set / c.Get)

Middleware часто записує дані в контекст для використання в обробниках:

// Middleware для додавання інформації про користувача
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        
        // Симуляція перевірки токена
        if token == "valid-token" {
            c.Set("user_id", 42)
            c.Set("username", "john_doe")
            c.Next() // Передаємо керування далі
        } else {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
                "error": "unauthorized",
            })
        }
    }
}

// Handler, що використовує дані з контексту
r.Use(AuthMiddleware())
r.GET("/profile", func(c *gin.Context) {
    userID, _ := c.Get("user_id")
    username := c.MustGet("username").(string)
    
    c.JSON(http.StatusOK, gin.H{
        "user_id":  userID,
        "username": username,
    })
})
Метод c.MustGet() викликає panic, якщо ключ не знайдено. Використовуйте c.Get(), якщо потрібна безпечна перевірка наявності.
Нові методи в Gin 1.12.0:
  • c.Delete(key) — видаляє значення з контексту за ключем
  • c.GetError() — повертає останню помилку з контексту (або nil)
  • c.GetErrorSlice() — повертає всі накопичені помилки як зріз
Приклад використання:
func ErrorLogger() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
        
        // Отримання всіх помилок після обробки
        if errors := c.GetErrorSlice(); len(errors) > 0 {
            for _, err := range errors {
                log.Printf("Request error: %v", err)
            }
        }
    }
}

Доступ до стандартних *http.Request та *http.ResponseWriter

Gin не приховує стандартні інтерфейси Go:

r.GET("/raw", func(c *gin.Context) {
    // Доступ до оригінального http.Request
    method := c.Request.Method
    url := c.Request.URL.String()
    
    // Доступ до ResponseWriter
    c.Writer.WriteHeader(http.StatusOK)
    c.Writer.Write([]byte("Direct write"))
    
    c.String(http.StatusOK, "Method: %s, URL: %s", method, url)
})
Якщо ви безпосередньо викликали c.Writer.WriteHeader() або c.Writer.Write(), НЕ викликайте після цього методи c.JSON() чи c.String() — це призведе до повторного встановлення заголовків та помилки.

Маршрутизація та HttpRouter

Gin використовує бібліотеку HttpRouter — один із найшвидших роутерів для Go, що базується на структурі даних Radix Tree (compressed prefix tree).

Як працює Radix Tree

Radix Tree оптимізує пошук маршрутів, зберігаючи спільні префікси в одному вузлі:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF

rectangle "GET /" as root #E2E8F0
rectangle "/users" as users #DBEAFE {
    rectangle "/:id" as userID #DCFCE7
    rectangle "/profile" as profile #FEF3C7
}
rectangle "/api" as api #DBEAFE {
    rectangle "/v1" as v1 #DCFCE7 {
        rectangle "/products" as products #FEF3C7
    }
}

root -down-> users
root -down-> api
users -down-> userID
users -down-> profile
api -down-> v1
v1 -down-> products

note right of userID
  Handler: GetUserByID
end note

note right of profile
  Handler: GetUserProfile
end note

note right of products
  Handler: ListProducts
end note

@enduml

Переваги Radix Tree:

  1. O(k) складність пошуку, де k — довжина шляху (а не кількість маршрутів).
  2. Мінімальне споживання пам'яті за рахунок компресії спільних префіксів.
  3. Підтримка динамічних параметрів (:id, *filepath) без регулярних виразів.
Gin підтримує автоматичне виправлення шляхів: якщо клієнт запитує /users/ (з кінцевим слешем), а маршрут зареєстрований як /users, Gin виконає редирект (якщо увімкнено RedirectTrailingSlash).

Реєстрація маршрутів

Gin надає методи для всіх стандартних HTTP-методів:

r := gin.Default()

r.GET("/resource", handlerFunc)       // Читання ресурсу
r.POST("/resource", handlerFunc)      // Створення ресурсу
r.PUT("/resource/:id", handlerFunc)   // Повна заміна ресурсу
r.PATCH("/resource/:id", handlerFunc) // Часткове оновлення
r.DELETE("/resource/:id", handlerFunc)// Видалення
r.OPTIONS("/resource", handlerFunc)   // Опції CORS
r.HEAD("/resource", handlerFunc)      // Лише заголовки (без тіла)

Множинні handler для одного маршруту

Gin дозволяє зареєструвати кілька handler (middleware + фінальний handler):

func Logger() gin.HandlerFunc {
    return func(c *gin.Context) {
        t := time.Now()
        c.Next() // Виконати наступні handler
        latency := time.Since(t)
        log.Printf("Request took %v", latency)
    }
}

func Auth() gin.HandlerFunc {
    return func(c *gin.Context) {
        token := c.GetHeader("Authorization")
        if token != "valid" {
            c.AbortWithStatus(http.StatusUnauthorized)
            return
        }
        c.Next()
    }
}

r.GET("/admin", Logger(), Auth(), func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{"message": "admin panel"})
})

Порядок виконання:

  1. Logger() → c.Next() → переходить до Auth.
  2. Auth() → c.Next() → переходить до фінального handler.
  3. Фінальний handler → відповідь клієнту.
  4. Повернення в Auth() (після c.Next()).
  5. Повернення в Logger() → обчислення latency.

Групування маршрутів (RouterGroup)

У реальних API маршрути мають спільні префікси (/api/v1/users, /api/v1/products) та однакові middleware (автентифікація, CORS). Gin надає RouterGroup для логічного групування.

Базове групування

r := gin.Default()

// Група для публічних ендпоінтів
public := r.Group("/api/v1")
{
    public.GET("/health", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"status": "ok"})
    })
    
    public.POST("/register", func(c *gin.Context) {
        c.JSON(http.StatusCreated, gin.H{"message": "user registered"})
    })
}

// Група для приватних ендпоінтів із middleware авторизації
private := r.Group("/api/v1")
private.Use(AuthMiddleware()) // Застосовуємо middleware лише до цієї групи
{
    private.GET("/profile", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "user profile"})
    })
    
    private.POST("/logout", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"message": "logged out"})
    })
}

Результат:

  • GET /api/v1/health — без авторизації.
  • GET /api/v1/profile — з авторизацією (спрацює AuthMiddleware).
Фігурні дужки {} навколо маршрутів групи є чисто стилістичними (для читабельності коду). Вони не мають функціонального значення в Go.

Вкладені групи

Групи можна вкладати для створення багаторівневих ієрархій:

r := gin.Default()

api := r.Group("/api")
{
    v1 := api.Group("/v1")
    {
        users := v1.Group("/users")
        {
            users.GET("", listUsers)
            users.GET("/:id", getUser)
            users.POST("", createUser)
            users.PUT("/:id", updateUser)
            users.DELETE("/:id", deleteUser)
        }
        
        products := v1.Group("/products")
        {
            products.GET("", listProducts)
            products.GET("/:id", getProduct)
        }
    }
    
    v2 := api.Group("/v2")
    {
        v2.GET("/users", listUsersV2)
    }
}

Результат:

GET    /api/v1/users
GET    /api/v1/users/:id
POST   /api/v1/users
PUT    /api/v1/users/:id
DELETE /api/v1/users/:id
GET    /api/v1/products
GET    /api/v1/products/:id
GET    /api/v2/users

Middleware на рівні групи

Middleware можна застосовувати до всіх маршрутів групи:

r := gin.New() // Без вбудованих middleware

// Глобальний middleware для всіх запитів
r.Use(gin.Logger())
r.Use(gin.Recovery())

// CORS лише для API
api := r.Group("/api")
api.Use(CorsMiddleware())
{
    // Авторизація лише для /api/admin
    admin := api.Group("/admin")
    admin.Use(AuthMiddleware())
    {
        admin.GET("/users", listUsers)
        admin.DELETE("/users/:id", deleteUser)
    }
}

Ланцюжок middleware для запиту GET /api/admin/users:

  1. gin.Logger() (глобальний).
  2. gin.Recovery() (глобальний).
  3. CorsMiddleware() (група /api).
  4. AuthMiddleware() (група /api/admin).
  5. listUsers (фінальний handler).
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #FFFFFF
autonumber

participant "Client" as C
participant "Logger" as L #DBEAFE
participant "Recovery" as R #E0F2FE
participant "CORS" as CO #FEF3C7
participant "Auth" as A #DCFCE7
participant "Handler" as H #BBF7D0

C -> L : Request
L -> R : c.Next()
R -> CO : c.Next()
CO -> A : c.Next()
A -> H : c.Next()
H --> A : Response
A --> CO : (after c.Next)
CO --> R : (after c.Next)
R --> L : (after c.Next)
L --> C : Response + Logs

@enduml

Створення власних Middleware

Middleware у Gin — це звичайна функція, що повертає gin.HandlerFunc. Розглянемо типові патерни створення middleware.

Шаблон middleware

func MyMiddleware() gin.HandlerFunc {
    // Код ініціалізації (виконується один раз при реєстрації)
    
    return func(c *gin.Context) {
        // Код, що виконується перед обробкою запиту
        
        c.Next() // Передаємо керування наступному handler
        
        // Код, що виконується після обробки запиту
    }
}

Приклад 1: Request ID для трейсингу

Додамо унікальний ID до кожного запиту для логування та відстеження:

import (
    "github.com/gin-gonic/gin"
    "github.com/google/uuid"
)

func RequestIDMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // Генеруємо унікальний ID
        requestID := uuid.New().String()
        
        // Зберігаємо в контексті
        c.Set("request_id", requestID)
        
        // Додаємо в заголовок відповіді
        c.Header("X-Request-ID", requestID)
        
        c.Next()
    }
}

// Використання
r := gin.New()
r.Use(RequestIDMiddleware())

r.GET("/test", func(c *gin.Context) {
    reqID := c.MustGet("request_id").(string)
    c.JSON(200, gin.H{"request_id": reqID})
})

Приклад 2: Rate Limiting

Обмежимо кількість запитів від одного IP-адреси:

import (
    "net/http"
    "sync"
    "time"

    "github.com/gin-gonic/gin"
    "golang.org/x/time/rate"
)

func RateLimitMiddleware(requestsPerSecond int) gin.HandlerFunc {
    type client struct {
        limiter  *rate.Limiter
        lastSeen time.Time
    }

    var (
        mu      sync.Mutex
        clients = make(map[string]*client)
    )

    // Горутина для очищення застарілих клієнтів
    go func() {
        for {
            time.Sleep(time.Minute)
            mu.Lock()
            for ip, c := range clients {
                if time.Since(c.lastSeen) > 3*time.Minute {
                    delete(clients, ip)
                }
            }
            mu.Unlock()
        }
    }()

    return func(c *gin.Context) {
        ip := c.ClientIP()

        mu.Lock()
        if _, exists := clients[ip]; !exists {
            clients[ip] = &client{
                limiter: rate.NewLimiter(rate.Limit(requestsPerSecond), requestsPerSecond),
            }
        }
        clients[ip].lastSeen = time.Now()
        limiter := clients[ip].limiter
        mu.Unlock()

        if !limiter.Allow() {
            c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
                "error": "rate limit exceeded",
            })
            return
        }

        c.Next()
    }
}

// Використання: максимум 10 запитів на секунду
r.Use(RateLimitMiddleware(10))
Бібліотека golang.org/x/time/rate реалізує алгоритм Token Bucket, який згладжує короткочасні сплески навантаження.

Приклад 3: CORS Middleware

Додамо підтримку Cross-Origin Resource Sharing для взаємодії з веб-клієнтами:

func CORSMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
        c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
        c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
        c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")

        // Обробка preflight-запиту (OPTIONS)
        if c.Request.Method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
            return
        }

        c.Next()
    }
}
Для продакшн-систем рекомендується використовувати готову бібліотеку github.com/gin-contrib/cors, яка підтримує більше налаштувань (wildcards, max-age, expose-headers).

Приклад 4: Logger з детальною інформацією

Створимо власний логер, що виводить метод, шлях, статус-код та час виконання:

import (
    "log"
    "time"

    "github.com/gin-gonic/gin"
)

func CustomLogger() gin.HandlerFunc {
    return func(c *gin.Context) {
        startTime := time.Now()
        path := c.Request.URL.Path
        method := c.Request.Method

        c.Next() // Виконуємо запит

        latency := time.Since(startTime)
        statusCode := c.Writer.Status()
        clientIP := c.ClientIP()

        log.Printf("[%s] %s %s %d %v %s",
            method,
            path,
            clientIP,
            statusCode,
            latency,
            c.Errors.ByType(gin.ErrorTypePrivate).String(),
        )
    }
}

Приклад 5: Timeout Middleware

Обмежимо час виконання handler через context.WithTimeout:

import (
    "context"
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
)

func TimeoutMiddleware(timeout time.Duration) gin.HandlerFunc {
    return func(c *gin.Context) {
        ctx, cancel := context.WithTimeout(c.Request.Context(), timeout)
        defer cancel()

        // Замінюємо контекст запиту
        c.Request = c.Request.WithContext(ctx)

        // Канал для сигналу завершення
        finished := make(chan struct{})
        
        go func() {
            c.Next()
            close(finished)
        }()

        select {
        case <-finished:
            // Handler завершився вчасно
        case <-ctx.Done():
            // Таймаут вичерпано
            c.AbortWithStatusJSON(http.StatusRequestTimeout, gin.H{
                "error": "request timeout",
            })
        }
    }
}

// Використання: максимум 5 секунд на обробку запиту
r.Use(TimeoutMiddleware(5 * time.Second))
Важливо: Цей middleware не зупиняє виконання handler після таймауту! Він лише надсилає відповідь 408. Для справжньої зупинки handler має перевіряти c.Request.Context().Done() всередині своєї логіки.

Керування потоком виконання

Gin надає три ключові методи для контролю ланцюжка middleware:

МетодПризначення
c.Next()Явно передає керування наступному handler. Код після c.Next() виконається після всіх наступних handler.
c.Abort()Встановлює прапорець, що припиняє виконання наступних handler. Код після c.Abort() у поточному middleware продовжить виконуватися.
c.AbortWithStatus(code)Комбінація c.Abort() + встановлення статус-коду.
c.AbortWithStatusJSON(code, obj)Комбінація c.Abort() + JSON-відповідь.

Приклад порівняння:

func MiddlewareA() gin.HandlerFunc {
    return func(c *gin.Context) {
        log.Println("A: before Next")
        c.Next()
        log.Println("A: after Next")
    }
}

func MiddlewareB() gin.HandlerFunc {
    return func(c *gin.Context) {
        log.Println("B: before Abort")
        c.Abort()
        log.Println("B: after Abort") // Це виконається!
    }
}

func Handler() gin.HandlerFunc {
    return func(c *gin.Context) {
        log.Println("Handler executed") // Це НЕ виконається
    }
}

r.GET("/test", MiddlewareA(), MiddlewareB(), Handler())

Вивід у консоль:

A: before Next
B: before Abort
B: after Abort
A: after Next
Після виклику c.Abort()завжди пишіть return, якщо хочете зупинити виконання поточної функції:
if unauthorized {
    c.AbortWithStatus(http.StatusUnauthorized)
    return // Без цього код продовжить виконуватися!
}

Практичний приклад: REST API з групуванням та middleware

Створимо повноцінний API для керування користувачами з різними рівнями доступу.

Структура проєкту

Тестування API

# Health check
curl http://localhost:8080/api/v1/health

# Реєстрація
curl -X POST http://localhost:8080/api/v1/auth/register \
  -H "Content-Type: application/json" \
  -d '{"email":"user@test.com","password":"secret"}'

# Логін
curl -X POST http://localhost:8080/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@test.com","password":"secret"}'

Логи сервера

Приклад логів
[GIN-debug] Listening and serving HTTP on :8080
[POST] /api/v1/auth/login 127.0.0.1 | Status: 200 | Latency: 2.341ms | ReqID: a7f3c8e1-2b4d
[GET] /api/v1/profile 127.0.0.1 | Status: 200 | Latency: 1.123ms | ReqID: b8a2d9f3-4c5e
[GET] /api/v1/admin/users 127.0.0.1 | Status: 403 | Latency: 0.854ms | ReqID: c9b3e0g4-5d6f

Порівняння Gin з іншими підходами

Для закріплення розуміння зведемо переваги та недоліки різних підходів до створення HTTP-серверів у Go:

Критерійnet/httpChiGinFiber
ШвидкістьСередняВисокаДуже високаЕкстремальна
СкладністьНизькаНизькаСередняСередня
MiddlewareРучна реалізаціяВбудовані + кастомніВбудовані + пул ContextВбудовані
Binding/Validation❌ Немає❌ Немає✅ Вбудовано✅ Вбудовано
РендерингРучнийБібліотекиJSON/XML/HTMLJSON/XML/HTML
Спільність зі стандартом100%100% (http.Handler)100% (http.Handler)❌ Власний API (fasthttp)
ВикористанняБазові сервісиREST APIREST API, microservicesHigh-performance API
Коли використовувати Gin:
  • Потрібна висока продуктивність без відмови від стандартної бібліотеки.
  • Проєкт вимагає швидкого прототипування REST API.
  • Необхідна вбудована валідація та зв'язування даних.
  • Команда віддає перевагу мінімалізму з корисними утилітами.
Коли НЕ використовувати Gin:
  • Потрібен повний контроль над кожним байтом (краще чистий net/http).
  • Проєкт використовує специфічні middleware екосистеми Chi (хоча їх можна адаптувати).
  • Команда вважає за краще явну архітектуру без «магії» фреймворку.

Підсумки

У цій лекції ми познайомилися з фреймворком Gin — потужним інструментом для швидкої розробки вебзастосунків на Go.

✅ Що ми вивчили

  • Архітектуру Gin: Engine, Context, HttpRouter, sync.Pool.
  • Життєвий цикл обробки запиту та оптимізації пам'яті.
  • Роботу з gin.Context: параметри, query, форми, заголовки, відповіді.
  • Маршрутизацію на основі Radix Tree та групування маршрутів.
  • Створення власних middleware та керування потоком виконання.
  • Інтеграцію глобальних, групових та специфічних middleware.

📚 Наступні кроки

У наступній лекції розглянемо:

  • Автоматичне зв'язування даних (Model Binding) через теги структур.
  • Декларативну валідацію за допомогою go-playground/validator.
  • Завантаження файлів через multipart/form-data.
  • Автентифікацію на основі JWT токенів.
  • Автоматичну генерацію документації Swagger/OpenAPI.

Контрольні запитання для самоперевірки

Додаткові ресурси

Copyright © 2026