Глибоке занурення в Gin: провідний фреймворк Golang

pic

pic

Gin — це HTTP веб-фреймворк, написаний на Go (Golang). Він має API, схожий на Martini, але з продуктивністю, яка в 40 разів швидша за Martini. Якщо вам потрібна неймовірна продуктивність, спробуйте Gin.

Офіційний вебсайт Gin представляє себе як веб-фреймворк з “високою продуктивністю” та “високою ефективністю”. Там також згадуються дві інші бібліотеки. Перша з них — це Martini, який також є веб-фреймворком і має назву алкогольного напою. Gin зазначає, що використовує API Martini, але працює в 40 разів швидше. Використання httprouter є важливою причиною того, чому Gin може бути в 40 разів швидшим за Martini.
Серед "Особливостей" на офіційному вебсайті вказано вісім ключових характеристик, і ми поступово розглянемо реалізацію цих функцій пізніше.

  • Швидкість
  • Підтримка Middleware
  • Без аварій
  • Перевірка JSON
  • Групування маршрутів
  • Управління помилками
  • Вбудоване/розширюване рендеринг

Почнемо з малого прикладу

Розглянемо найменший приклад, наданий в офіційній документації.

package main  

import "github.com/gin-gonic/gin"  
func main() {  
 r := gin.Default()  
 r.GET("/ping", func(c *gin.Context) {  
 c.JSON(200, gin.H{  
 "message": "pong",  
 })  
 })  
 r.Run() // listen and serve on 0.0.0.0:8080  
}

Запустіть цей приклад, а потім використовуйте браузер, щоб перейти за адресою http://localhost:8080/ping, і ви отримаєте "pong". Цей приклад дуже простий. Його можна розділити лише на три кроки:

  1. Використовуємо gin.Default(), щоб створити об'єкт Engine з налаштуваннями за замовчуванням.
    2.
    Зареєструйте функцію зворотного виклику для адреси “/ping” у методі GET об'єкта Engine. Ця функція поверне "pong".
  2. Запустіть Engine, щоб почати слухати порт і надавати послуги.

HTTP Метод

З методу GET у наведеному вище прикладі можна побачити, що в Gin методи обробки HTTP методів потрібно реєструвати за допомогою відповідних функцій з тими самими іменами. Є дев'ять HTTP методів, і чотири найпоширеніші — це GET, POST, PUT і DELETE, які відповідають функціям запиту, вставки, оновлення і видалення відповідно. Варто зазначити, що Gin також надає інтерфейс Any, який дозволяє безпосередньо прив'язати обробку всіх методів HTTP до однієї адреси. Повернутий результат зазвичай містить дві або три частини. code та message завжди присутні, а data зазвичай використовується для представлення додаткових даних. Якщо немає додаткових даних для повернення, його можна опустити.
У наведеному прикладі 200 — це значення поля code, а "pong" — це значення поля message.

Створення змінної Engine

У наведеному вище прикладі було використано gin.Default() для створення Engine. Однак ця функція є обгорткою для New. Насправді, Engine створюється через інтерфейс New.

func New() *Engine {  
 debugPrintWARNINGNew()  
 engine := &Engine{  
 RouterGroup: RouterGroup{  
 //... Ініціалізація полів RouterGroup  
 },  
 //... Ініціалізація решти полів  
 }  
 engine.RouterGroup.engine = engine // Збереження вказівника на engine в RouterGroup  
 engine.pool.New = func() any {  
 return engine.allocateContext()  
 }  
 return engine  
}

Зараз можна коротко ознайомитись з процесом створення, не звертаючи уваги на значення різних членів структури Engine.
Можна побачити, що крім створення та ініціалізації змінної engine типу Engine, функція New також встановлює engine.pool.New як анонімну функцію, яка викликає engine.allocateContext(). Функція цієї функції буде розглянута пізніше.

Реєстрація функцій зворотного виклику для маршрутів

У структурі Engine є вкладена структура RouterGroup. Інтерфейси, пов'язані з HTTP методами Engine, всі успадковані від RouterGroup. "Групування маршрутів" у функціональних можливостях, згаданих на офіційному вебсайті, реалізується через структуру RouterGroup.

type RouterGroup struct {  
 Handlers HandlersChain // Функції обробки для самостійної групи  
 basePath string // Пов'язаний базовий шлях  
 engine *Engine // Збереження пов'язаного об'єкта engine  
 root bool // Прапор кореня, тільки той, що створений за замовчуванням в Engine є true  
}

Кожен RouterGroup асоціюється з базовим шляхом basePath. basePath для RouterGroup, вбудованого в Engine, дорівнює "/".
Також існує набір функцій обробки Handlers. Всі запити до шляхів, асоційованих з цією групою, додатково виконуватимуть функції обробки цієї групи, які в основному використовуються для викликів проміжного програмного забезпечення (middleware). Handlers дорівнює nil при створенні Engine, і набір функцій може бути імпортований через метод Use. Ми побачимо це використання пізніше.

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {  
 absolutePath := group.calculateAbsolutePath(relativePath)  
 handlers = group.combineHandlers(handlers)  
 group.engine.addRoute(httpMethod, absolutePath, handlers)  
 return group.returnObj()  
}

Метод handle у RouterGroup є кінцевим етапом для реєстрації всіх функцій зворотного виклику HTTP методів. Методи GET та інші методи, що стосуються HTTP, які викликаються в початковому прикладі, є просто обгортками для методу handle.
Метод handle обчислює абсолютний шлях, враховуючи basePath з RouterGroup і параметр відносного шляху, одночасно викликаючи метод combineHandlers, щоб отримати фінальний масив handlers. Ці результати передаються як параметри в метод addRoute об'єкта Engine для реєстрації функцій обробки.

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {  
 finalSize := len(group.Handlers) + len(handlers)  
 assert1(finalSize < int(abortIndex), "too many handlers")  
 mergedHandlers := make(HandlersChain, finalSize)  
 copy(mergedHandlers, group.Handlers)  
 copy(mergedHandlers[len(group.Handlers):], handlers)  
 return mergedHandlers  
}

Що робить метод combineHandlers: він створює зріз mergedHandlers, копіює в нього Handlers з самого RouterGroup, потім копіює handlers з параметрів і, врешті-решт, повертає mergedHandlers.
Тобто, коли реєструється будь-який метод за допомогою handle, фактичний результат включає в себе Handlers самого RouterGroup.

Використання дерева радікс для прискорення отримання маршрутів

У пункті "Швидкість" на офіційному сайті згадується, що маршрутизація мережевих запитів реалізована на основі дерева радікс (Radix Tree). Цю частину не реалізує сам Gin, а використовує бібліотеку httprouter, яка була згадана на початку введення в Gin. Gin застосовує httprouter для досягнення цієї функціональності. Реалізацію дерева радікс тут не будемо розглядати, зосередимось лише на його використанні. Можливо, пізніше ми напишемо окрему статтю про реалізацію дерева радікс. В Engine є змінна trees, яка є зрізом структури methodTree.
Ця змінна зберігає посилання на всі дерева радікс.

type methodTree struct {  
 method string // Назва методу  
 root *node // Посилання на корінь дерева пов'язаних списків  
}

Engine підтримує дерево радікс для кожного HTTP-методу. Корінь цього дерева та назва методу зберігаються разом у змінній methodTree, а всі змінні methodTree знаходяться в масиві trees.

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {  
 //... Опущено деякий код  
 root := engine.trees.get(method)  
 if root == nil {  
 root = new(node)  
 root.fullPath = "/"  
 engine.trees = append(engine.trees, methodTree{method: method, root: root})  
 }  
 root.addRoute(path, handlers)  
 //... Опущено деякий код  
}

Як можна побачити, в методі addRoute класу Engine, спочатку використовується метод get з trees, щоб отримати корінь дерева радікс, що відповідає заданому method.
Якщо корінь дерева радікс не отримано, це означає, що для цього method ще не було зареєстровано жодного методу, і буде створено вузол дерева, який стане коренем дерева, а також буде додано до trees. Після отримання кореня дерева, використовується метод addRoute цього кореня для реєстрації набору обробників handlers для шляху path. Цей крок передбачає створення вузла для path і handlers та збереження його в дереві радікс. Якщо спробувати зареєструвати вже зареєстровану адресу, метод addRoute безпосередньо викине помилку panic. При обробці HTTP-запиту необхідно знайти значення відповідного вузла через path. Корінь дерева має метод getValue, який відповідає за обробку операцій пошуку. Ми згадаємо про це під час опису обробки HTTP-запитів у Gin.

Імпорт обробників проміжного програмного забезпечення (Middleware)

Метод Use класу RouterGroup дозволяє імпортувати набір обробників проміжного програмного забезпечення (middleware).
Підтримка проміжного програмного забезпечення (Middleware) в пункті функцій, згаданих на офіційному сайті, досягається через метод Use. У початковому прикладі, коли створювалася змінна структури Engine, не використовувався метод New, а використовувався Default. Давайте подивимося, що додатково робить Default.

func Default() *Engine {  
 debugPrintWARNINGDefault() // Виведення логу  
 engine := New() // Створення об'єкта  
 engine.Use(Logger(), Recovery()) // Імпорт функцій обробки проміжного програмного забезпечення  
 return engine  
}

Як видно, це дуже проста функція. Окрім виклику New для створення об'єкта Engine, вона лише викликає Use для імпорту значень, які повертають дві функції проміжного програмного забезпечення, Logger та Recovery. Повернуте значення Logger є функцією для ведення логів, а повернуте значення Recovery — функцією для обробки panic. Зараз ми пропустимо це і розглянемо ці дві функції пізніше.
Хоча структура Engine вбудовує RouterGroup, вона також реалізує метод Use, але це просто виклик методу Use з RouterGroup з деякими допоміжними операціями.

func (engine *Engine) Use(middleware...HandlerFunc) IRoutes {  
 engine.RouterGroup.Use(middleware...)  
 engine.rebuild404Handlers()  
 engine.rebuild405Handlers()  
 return engine  
}  

func (group *RouterGroup) Use(middleware...HandlerFunc) IRoutes {  
 group.Handlers = append(group.Handlers, middleware...)  
 return group.returnObj()  
}

Як видно, метод Use в RouterGroup також дуже простий. Він просто додає функції обробки проміжного програмного забезпечення (middleware) з параметрів до свого власного масиву Handlers через append.

Початок роботи

У малому прикладі останнім кроком є виклик методу Run з Engine без параметрів.
Після виклику весь фреймворк починає працювати, і при відвідуванні зареєстрованої адреси через браузер коректно спрацьовує зворотний виклик.

func (engine *Engine) Run(addr...string) (err error) {  
 //... Пропущено деякий код  
 address := resolveAddress(addr) // Парсинг адреси, за замовчуванням адреса 0.0.0.0:8080  
 debugPrint("Listening and serving HTTP on %s\n", address)  
 err = http.ListenAndServe(address, engine.Handler())  
 return  
}

Метод Run виконує лише дві операції: парсить адресу і запускає сервер. Тут адреса фактично приймає лише рядок, але для досягнення ефекту можливості передавати або не передавати параметр використовується змінний параметр. Метод resolveAddress обробляє різні варіанти ситуації з addr. Запуск сервісу використовує метод ListenAndServe з пакету net/http стандартної бібліотеки. Цей метод приймає адресу для прослуховування та змінну інтерфейсу Handler (Обробник).
Опис інтерфейсу Handler (Обробник) дуже простий, він містить лише один метод ServeHTTP.

func ListenAndServe(addr string, handler Handler) error {  
 server := &Server{Addr: addr, Handler: handler}  
 return server.ListenAndServe()  
}  

type Handler interface {  
 ServeHTTP(ResponseWriter, *Request)  
}

Оскільки Engine реалізує метод ServeHTTP, сам об'єкт Engine буде переданий в метод ListenAndServe. Коли на моніторинговому порту з'являється нове з'єднання, метод ListenAndServe буде відповідати за прийом і встановлення з'єднання, а коли на з'єднанні з'являться дані, він викликає метод ServeHTTP обробника для їх обробки.

Обробка повідомлень

Метод ServeHTTP в Engine є функцією зворотного виклику для обробки повідомлень.
Давайте подивимось на його вміст.

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {  
 c := engine.pool.Get().(*Context)   
 c.writermem.reset(w)  
 c.Request = req  
 c.reset()  
 engine.handleHTTPRequest(c)   
 engine.pool.Put(c)   
}

Функція зворотного виклику має два параметри. Перший — це w, який використовується для отримання відповіді на запит. Дані відповіді записуються у w. Другий параметр — це req, який містить дані цього запиту. Усі дані, необхідні для подальшої обробки, можна отримати з req. Метод ServeHTTP виконує чотири дії. По-перше, отримує Context з пулу pool, потім прив'язує цей Context до параметрів функції зворотного виклику, після чого викликає метод handleHTTPRequest, передаючи Context як параметр для обробки цього мережевого запиту, і нарешті повертає Context назад у пул. Давайте поки що розглянемо лише основну частину методу handleHTTPRequest.

func (engine *Engine) handleHTTPRequest(c *Context) {  
 //...
Пропустимо деякий код  
 t := engine.trees  
 for i, tl := 0, len(t); i < tl; i++ {  
 if t[i].method != httpMethod {  
 continue  
 }  
 root := t[i].root  
 // Знайти маршрут у дереві  
 value := root.getValue(rPath, c.params, c.skippedNodes, unescape)  
 //... Пропустимо деякий код  
 if value.handlers != nil {  
 c.handlers = value.handlers  
 c.fullPath = value.fullPath  
 c.Next()  
 c.writermem.WriteHeaderNow()  
 return  
 }  
 //... Пропустимо деякий код  
 }  
 //... Пропустимо деякий код  
}

Метод handleHTTPRequest головним чином виконує дві дії. По-перше, він отримує раніше зареєстрований метод з дерева радікс за адресою запиту. Тут handlers будуть призначені для обробки цього запиту в Context, після чого викликається функція Next у Context для виконання методів у масиві handlers.
Нарешті, запишіть дані відповіді цього запиту в об'єкт типу responseWriter, який знаходиться в Context.

Context

Під час обробки HTTP-запиту всі дані, що стосуються контексту, знаходяться в змінній Context. Автор також зазначив у коментарі до структури Context, що "Context — це найважливіша частина gin", що підкреслює її значення. Коли ми говоримо про метод ServeHTTP структури Engine вище, можна помітити, що Context не створюється безпосередньо, а отримується через метод Get змінної pool структури Engine. Після того, як він забраний, його стан скидається перед використанням, а після використання він повертається назад у пул. Змінна pool структури Engine має тип sync.Pool. Наразі просто знайте, що це об'єктний пул, наданий офіційно Go, який підтримує паралельне використання. Ви можете отримати об'єкт з пулу за допомогою його методу Get, а також повернути об'єкт у пул за допомогою методу Put.
Коли пул порожній і використовується метод Get, він створює об'єкт через свій власний метод New та повертає його. Цей метод New визначений у методі New структури Engine. Давайте ще раз поглянемо на метод New структури Engine.

func New() *Engine {  
 //... Омітити інший код  
 engine.pool.New = func() any {  
 return engine.allocateContext()  
 }  
 return engine  
}

З коду можна побачити, що метод створення Context — це метод allocateContext структури Engine. У методі allocateContext немає нічого загадкового.
Цей метод просто виконує двоетапне попереднє виділення довжин зрізів, а потім створює об'єкт і повертає його.

func (engine *Engine) allocateContext() *Context {  
 v := make(Params, 0, engine.maxParams)  
 skippedNodes := make([]skippedNode, 0, engine.maxSections)  
 return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}  
}

Метод Next структури Context, згаданий вище, виконає всі методи в масиві handlers.
Подивимося на його реалізацію.

func (c *Context) Next() {  
 c.index++  
 for c.index < int8(len(c.handlers)) {  
 c.handlers[c.index](c)  
 c.index++  
 }  
}

Хоча handlers є зрізом, метод Next не просто реалізований як перебір handlers, а вводить запис про прогрес обробки index, який ініціалізується значенням 0, збільшується на початку методу і знову збільшується після виконання методу.

Дизайн методу Next тісно пов'язаний з його використанням, в основному для взаємодії з деякими middleware функціями.
Наприклад, коли під час виконання певного handler виникає panic, помилку можна перехопити за допомогою recover у middleware, а потім знову викликати Next, щоб продовжити виконання наступних handlers без того, щоб проблема з одним handler вплинула на всю масив handlers.

Обробка Panic

У Gin, якщо функція обробки певного запиту викликає panic, весь фреймворк не падає відразу. Натомість виводиться повідомлення про помилку, і сервіс продовжує працювати. Це схоже на те, як Lua фреймворки зазвичай використовують xpcall для виконання функцій обробки повідомлень. Це операція є особливістю "Без аварій" (Crash-free), яка згадується в офіційній документації. Як згадувалося раніше, при використанні gin.Default для створення Engine, буде виконано метод Use цього Engine, щоб імпортувати дві функції. Одна з них — це повернуте значення функції Recovery, яка є обгорткою для інших функцій.
Остання викликана функція — це CustomRecoveryWithWriter. Давайте подивимося на реалізацію цієї функції.

func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {  
 //... Омити інший код  
 return func(c *Context) {  
 defer func() {  
 if err := recover(); err!= nil {  
 //... Код обробки помилки  
 }  
 }()  
 c.Next() // Виконати наступний handler  
 }  
}

Ми не зосереджуємось на деталях обробки помилок тут, а лише на тому, що ця функція робить. Ця функція повертає анонімну функцію. В цій анонімній функції за допомогою defer реєструється ще одна анонімна функція. У цій внутрішній анонімній функції використовується recover, щоб перехопити panic, а потім виконується обробка помилки.
Після завершення обробки викликається метод Next об'єкта Context, щоб продовжити виконання handlers цього контексту, які спочатку виконувались по черзі.

Leapcell: Платформа нового покоління для безсерверного хостингу веб-сайтів, асинхронних задач та Redis

Нарешті, хочу представити найкращу платформу для розгортання сервісів Gin: Leapcell.

pic

1. Підтримка кількох мов програмування

  • Розробляйте за допомогою JavaScript, Python, Go або Rust.

2. Розгортання необмеженої кількості проектів безкоштовно

  • Платіть тільки за використання — без запитів — без оплат.

3. Неперевершена ефективність витрат

  • Оплата за фактичне використання без витрат на простої.
  • Приклад: $25 підтримує 6.94 мільйона запитів за середній час відповіді 60 мс.

4.

Оптимізований досвід для розробників

  • Інтуїтивно зрозумілий інтерфейс для простого налаштування.
  • Повністю автоматизовані CI/CD пайплайни та інтеграція з GitOps.
  • Метрики в реальному часі та журналювання для отримання корисної інформації.

5. Легке масштабування та висока продуктивність

  • Автоматичне масштабування для легкого оброблення високої конкуренції.
  • Відсутність операційних витрат — просто зосередьтеся на розробці.

Детальніше читайте в Документації!

Twitter Leapcell: https://x.com/LeapcellHQ

Перекладено з: A Deep Dive into Gin: Golang’s Leading Framework

Leave a Reply

Your email address will not be published. Required fields are marked *