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". Цей приклад дуже простий. Його можна розділити лише на три кроки:
- Використовуємо
gin.Default()
, щоб створити об'єктEngine
з налаштуваннями за замовчуванням.
2.
Зареєструйте функцію зворотного виклику для адреси “/ping” у методіGET
об'єктаEngine
. Ця функція поверне "pong". - Запустіть
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.
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