Ein tiefer Einblick in Gin: Golangs führendes Framework
Min-jun Kim
Dev Intern · Leapcell

Einführung
Gin ist ein HTTP-Web-Framework, das in Go (Golang) geschrieben ist. Es bietet eine Martini-ähnliche API, jedoch mit einer bis zu 40-mal höheren Leistung als Martini. Wenn du eine umwerfende Leistung benötigst, hol dir Gin.
Die offizielle Website von Gin stellt sich als Web-Framework mit "hoher Leistung" und "guter Produktivität" vor. Sie erwähnt auch zwei weitere Bibliotheken. Die erste ist Martini, das ebenfalls ein Web-Framework ist und den Namen eines Likörs trägt. Gin gibt an, dessen API zu verwenden, aber 40-mal schneller zu sein. Die Verwendung von httprouter
ist ein wichtiger Grund, warum es 40-mal schneller als Martini sein kann.
Unter den "Features" auf der offiziellen Website werden acht Hauptmerkmale aufgeführt, deren Umsetzung wir im Folgenden schrittweise betrachten werden.
- Schnell
- Middleware-Unterstützung
- Absturzfrei
- JSON-Validierung
- Routen Gruppierung
- Fehlermanagement
- Rendering eingebaut/erweiterbar
Start mit einem kleinen Beispiel
Schauen wir uns das kleinste Beispiel an, das in der offiziellen Dokumentation gegeben ist.
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 }
Führe dieses Beispiel aus und besuche dann mit einem Browser http://localhost:8080/ping
, und du erhältst ein "pong".
Dieses Beispiel ist sehr einfach. Es lässt sich in nur drei Schritte unterteilen:
- Verwende
gin.Default()
, um einEngine
-Objekt mit Standardkonfigurationen zu erstellen. - Registriere eine Callback-Funktion für die Adresse "/ping" in der
GET
-Methode derEngine
. Diese Funktion gibt ein "pong" zurück. - Starte die
Engine
, um mit dem Abhören des Ports zu beginnen und Dienste bereitzustellen.
HTTP-Methode
Aus der GET
-Methode im obigen kleinen Beispiel geht hervor, dass in Gin die Verarbeitungsmethoden von HTTP-Methoden mit den entsprechenden Funktionen mit den gleichen Namen registriert werden müssen.
Es gibt neun HTTP-Methoden, und die vier am häufigsten verwendeten sind GET
, POST
, PUT
und DELETE
, die den vier Funktionen des Abfragens, Einfügens, Aktualisierens bzw. Löschens entsprechen. Es ist zu beachten, dass Gin auch die Any
-Schnittstelle bereitstellt, die alle HTTP-Methodenverarbeitungsmethoden direkt an eine Adresse binden kann.
Das zurückgegebene Ergebnis enthält im Allgemeinen zwei oder drei Teile. Der code
und die message
sind immer vorhanden, und data
wird im Allgemeinen verwendet, um zusätzliche Daten darzustellen. Wenn keine zusätzlichen Daten zurückgegeben werden müssen, kann es weggelassen werden. In dem Beispiel ist 200 der Wert des Feldes code
, und "pong" ist der Wert des Feldes message
.
Erstellen einer Engine-Variable
Im obigen Beispiel wurde gin.Default()
verwendet, um die Engine
zu erstellen. Diese Funktion ist jedoch ein Wrapper für New
. Tatsächlich wird die Engine
über die New
-Schnittstelle erstellt.
func New() *Engine { debugPrintWARNINGNew() engine := &Engine{ RouterGroup: RouterGroup{ //... Initialisiere die Felder von RouterGroup }, //... Initialisiere die restlichen Felder } engine.RouterGroup.engine = engine // Speichere den Zeiger der Engine in RouterGroup engine.pool.New = func() any { return engine.allocateContext() } return engine }
Wirf jetzt nur einen kurzen Blick auf den Erstellungsprozess und konzentriere dich nicht auf die Bedeutung der verschiedenen Member-Variablen in der Engine
-Struktur. Es ist ersichtlich, dass New
zusätzlich zum Erstellen und Initialisieren einer engine
-Variable vom Typ Engine
auch engine.pool.New
auf eine anonyme Funktion setzt, die engine.allocateContext()
aufruft. Die Funktion dieser Funktion wird später erläutert.
Registrieren von Routen-Callback-Funktionen
Es gibt eine eingebettete Struktur RouterGroup
in der Engine
. Die Schnittstellen, die sich auf HTTP-Methoden der Engine
beziehen, werden alle von RouterGroup
geerbt. Die "Routen Gruppierung" in den auf der offiziellen Website erwähnten Feature-Punkten wird über die RouterGroup
-Struktur erreicht.
type RouterGroup struct { Handlers HandlersChain // Verarbeitungsfunktionen der Gruppe selbst basePath string // Zugehöriger Basispfad engine *Engine // Speichere das zugehörige Engine-Objekt root bool // Root-Flag, nur das standardmäßig in Engine erstellte ist true }
Jede RouterGroup
ist mit einem Basispfad basePath
verbunden. Der basePath
der in die Engine
eingebetteten RouterGroup
ist "/".
Es gibt auch eine Reihe von Verarbeitungsfunktionen Handlers
. Alle Anfragen unter den mit dieser Gruppe verbundenen Pfaden führen zusätzlich die Verarbeitungsfunktionen dieser Gruppe aus, die hauptsächlich für Middleware-Aufrufe verwendet werden. Handlers
ist nil
, wenn die Engine
erstellt wird, und eine Reihe von Funktionen kann über die Use
-Methode importiert werden. Wir werden diese Verwendung später sehen.
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() }
Die handle
-Methode von RouterGroup
ist der endgültige Einstiegspunkt für die Registrierung aller HTTP-Methoden-Callback-Funktionen. Die GET
-Methode und andere Methoden im Zusammenhang mit HTTP-Methoden, die im anfänglichen Beispiel aufgerufen wurden, sind nur Wrapper für die handle
-Methode.
Die handle
-Methode berechnet den absoluten Pfad anhand des basePath
der RouterGroup
und des relativen Pfadparameters und ruft gleichzeitig die combineHandlers
-Methode auf, um das endgültige handlers
-Array zu erhalten. Diese Ergebnisse werden als Parameter an die addRoute
-Methode der Engine
übergeben, um die Verarbeitungsfunktionen zu registrieren.
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 }
Die combineHandlers
-Methode erstellt ein Slice mergedHandlers
, kopiert dann die Handlers
der RouterGroup
selbst hinein, kopiert dann die handlers
der Parameter hinein und gibt schließlich mergedHandlers
zurück. Das heißt, wenn eine Methode mit handle
registriert wird, enthält das tatsächliche Ergebnis die Handlers
der RouterGroup
selbst.
Verwende Radix-Baum, um den Routenabruf zu beschleunigen
In dem auf der offiziellen Website erwähnten Feature-Punkt "Schnell" wird erwähnt, dass das Routing von Netzwerkanfragen auf dem Radix-Baum (Radix Tree) basiert. Dieser Teil wird nicht von Gin implementiert, sondern von httprouter
, das zu Beginn der Einführung von Gin erwähnt wurde. Gin verwendet httprouter
, um diesen Teil der Funktion zu erreichen. Die Implementierung des Radix-Baums wird hier vorerst nicht erwähnt. Wir werden uns vorerst nur auf seine Verwendung konzentrieren. Vielleicht schreiben wir später einen separaten Artikel über die Implementierung des Radix-Baums.
In der Engine
gibt es eine Variable trees
, die ein Slice der methodTree
-Struktur ist. Diese Variable enthält Verweise auf alle Radix-Bäume.
type methodTree struct { method string // Name der Methode root *node // Zeiger auf den Root-Knoten der verlinkten Liste }
Die Engine
verwaltet einen Radix-Baum für jede HTTP-Methode. Der Root-Knoten dieses Baums und der Name der Methode werden zusammen in einer methodTree
-Variablen gespeichert, und alle methodTree
-Variablen befinden sich in trees
.
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { //... Einige Code weglassen 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) //... Einige Code weglassen }
Es ist ersichtlich, dass in der addRoute
-Methode der Engine
zuerst die get
-Methode von trees
verwendet wird, um den Root-Knoten des Radix-Baums abzurufen, der der method
entspricht. Wenn der Root-Knoten des Radix-Baums nicht abgerufen wird, bedeutet dies, dass für diese method
zuvor keine Methode registriert wurde, und ein Baumknoten wird als Root-Knoten des Baums erstellt und zu trees
hinzugefügt.
Nachdem der Root-Knoten abgerufen wurde, wird die addRoute
-Methode des Root-Knotens verwendet, um eine Reihe von Verarbeitungsfunktionen handlers
für den Pfad path
zu registrieren. Dieser Schritt besteht darin, einen Knoten für path
und handlers
zu erstellen und im Radix-Baum zu speichern. Wenn du versuchst, eine bereits registrierte Adresse zu registrieren, wirft addRoute
direkt einen panic
-Fehler.
Bei der Verarbeitung einer HTTP-Anfrage ist es notwendig, den Wert des entsprechenden Knotens über den path
zu finden. Der Root-Knoten verfügt über eine getValue
-Methode, die für die Verarbeitung des Abfragevorgangs zuständig ist. Wir werden dies erwähnen, wenn wir über die Gin-Verarbeitung von HTTP-Anfragen sprechen.
Importiere Middleware-Verarbeitungsfunktionen
Die Use
-Methode von RouterGroup
kann eine Reihe von Middleware-Verarbeitungsfunktionen importieren. Die "Middleware-Unterstützung" in den auf der offiziellen Website erwähnten Feature-Punkten wird durch die Use
-Methode erreicht.
Im anfänglichen Beispiel wurde beim Erstellen der Engine
-Strukturvariablen nicht New
, sondern Default
verwendet. Schauen wir uns an, was Default
zusätzlich tut.
func Default() *Engine { debugPrintWARNINGDefault() // Log ausgeben engine := New() // Objekt erstellen engine.Use(Logger(), Recovery()) // Middleware-Verarbeitungsfunktionen importieren return engine }
Es ist ersichtlich, dass es sich um eine sehr einfache Funktion handelt. Zusätzlich zum Aufrufen von New
, um das Engine
-Objekt zu erstellen, wird nur Use
aufgerufen, um die Rückgabewerte von zwei Middleware-Funktionen, Logger
und Recovery
, zu importieren. Der Rückgabewert von Logger
ist eine Funktion zur Protokollierung, und der Rückgabewert von Recovery
ist eine Funktion zur Verarbeitung von panic
. Wir werden dies vorerst überspringen und uns diese beiden Funktionen später ansehen.
Obwohl die Engine
RouterGroup
einbettet, implementiert sie auch die Use
-Methode, aber es ist nur ein Aufruf der Use
-Methode von RouterGroup
und einiger Hilfsvorgänge.
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() }
Es ist ersichtlich, dass die Use
-Methode von RouterGroup
auch sehr einfach ist. Sie fügt einfach die Middleware-Verarbeitungsfunktionen der Parameter über append
zu ihren eigenen Handlers
hinzu.
Starte Ausführung
Im kleinen Beispiel besteht der letzte Schritt darin, die Run
-Methode der Engine
ohne Parameter aufzurufen. Nach dem Aufruf beginnt das gesamte Framework zu laufen, und der Besuch der registrierten Adresse mit einem Browser kann den Callback korrekt auslösen.
func (engine *Engine) Run(addr...string) (err error) { //... Einige Code weglassen address := resolveAddress(addr) // Adresse parsen, die Standardadresse ist 0.0.0.0:8080 debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine.Handler()) return }
Die Run
-Methode macht nur zwei Dinge: die Adresse parsen und den Dienst starten. Hier muss die Adresse eigentlich nur einen String übergeben, aber um den Effekt zu erzielen, dass sie übergeben werden kann oder nicht, wird ein variadischer Parameter verwendet. Die resolveAddress
-Methode behandelt die Ergebnisse verschiedener Situationen von addr
.
Das Starten des Dienstes verwendet die ListenAndServe
-Methode im Paket net/http
der Standardbibliothek. Diese Methode akzeptiert eine Listening-Adresse und eine Variable der Handler
-Schnittstelle. Die Definition der Handler
-Schnittstelle ist sehr einfach und hat nur eine ServeHTTP
-Methode.
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } type Handler interface { ServeHTTP(ResponseWriter, *Request) }
Da die Engine
ServeHTTP
implementiert, wird die Engine
selbst hier an die ListenAndServe
-Methode übergeben. Wenn eine neue Verbindung zum überwachten Port besteht, ist ListenAndServe
für die Annahme und den Aufbau der Verbindung verantwortlich, und wenn Daten über die Verbindung vorhanden sind, wird die ServeHTTP
-Methode des handler
zur Verarbeitung aufgerufen.
Nachrichten verarbeiten
Das ServeHTTP
der Engine
ist die Callback-Funktion zur Verarbeitung von Nachrichten. Schauen wir uns den Inhalt an.
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) }
Die Callback-Funktion hat zwei Parameter. Der erste ist w
, der zum Empfangen der Anfrageantwort verwendet wird. Schreibe die Antwortdaten in w
. Der andere ist req
, der die Daten dieser Anfrage enthält. Alle für die nachfolgende Verarbeitung erforderlichen Daten können aus req
gelesen werden.
Die ServeHTTP
-Methode macht vier Dinge. Zuerst wird ein Context
aus dem pool
-Pool abgerufen, dann wird der Context
an die Parameter der Callback-Funktion gebunden, dann wird die handleHTTPRequest
-Methode mit dem Context
als Parameter aufgerufen, um diese Netzwerkanfrage zu verarbeiten, und schließlich wird der Context
wieder in den Pool zurückgelegt.
Betrachten wir zunächst nur den Kernteil der handleHTTPRequest
-Methode.
func (engine *Engine) handleHTTPRequest(c *Context) { //... Einige Code weglassen t := engine.trees for i, tl := 0, len(t); i < tl; i++ { if t[i].method!= httpMethod { continue } root := t[i].root // Route im Baum finden value := root.getValue(rPath, c.params, c.skippedNodes, unescape) //... Einige Code weglassen if value.handlers!= nil { c.handlers = value.handlers c.fullPath = value.fullPath c.Next() c.writermem.WriteHeaderNow() return } //... Einige Code weglassen } //... Einige Code weglassen }
Die handleHTTPRequest
-Methode macht hauptsächlich zwei Dinge. Zuerst wird die zuvor registrierte Methode anhand der Adresse der Anfrage aus dem Radix-Baum abgerufen. Hier werden die handlers
dem Context
für diese Verarbeitung zugewiesen, und dann wird die Next
-Funktion des Context
aufgerufen, um die Methoden in den handlers
auszuführen. Schließlich werden die Rückgabedaten dieser Anfrage in das Objekt vom Typ responseWriter
des Context
geschrieben.
Context
Bei der Verarbeitung einer HTTP-Anfrage befinden sich alle kontextbezogenen Daten in der Context
-Variable. Der Autor schrieb auch im Kommentar der Context
-Struktur, dass "Context der wichtigste Teil von Gin ist", was seine Bedeutung zeigt.
Wenn wir oben über die ServeHTTP
-Methode der Engine
sprechen, ist zu sehen, dass der Context
nicht direkt erstellt, sondern über die Get
-Methode der pool
-Variablen der Engine
abgerufen wird. Nach dem Herausnehmen wird sein Zustand vor der Verwendung zurückgesetzt und nach der Verwendung wieder in den Pool zurückgelegt.
Die pool
-Variable der Engine
ist vom Typ sync.Pool
. Fürs Erste wissen wir nur, dass es sich um einen Objektpool handelt, der von Go offiziell bereitgestellt wird und die gleichzeitige Verwendung unterstützt. Du kannst ein Objekt über seine Get
-Methode aus dem Pool abrufen und du kannst auch ein Objekt mit der Put
-Methode in den Pool legen. Wenn der Pool leer ist und die Get
-Methode verwendet wird, wird ein Objekt über seine eigene New
-Methode erstellt und zurückgegeben.
Diese New
-Methode ist in der New
-Methode der Engine
definiert. Schauen wir uns noch einmal die New
-Methode der Engine
an.
func New() *Engine { //... Anderen Code weglassen engine.pool.New = func() any { return engine.allocateContext() } return engine }
Aus dem Code geht hervor, dass die Erstellungsmethode des Context
die allocateContext
-Methode der Engine
ist. Es gibt kein Geheimnis in der allocateContext
-Methode. Es werden lediglich die Slice-Längen in zwei Schritten präallokiert, dann wird das Objekt erstellt und zurückgegeben.
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} }
Die oben erwähnte Next
-Methode des Context
führt alle Methoden in den handlers
aus. Schauen wir uns die Implementierung an.
func (c *Context) Next() { c.index++ for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } }
Obwohl handlers
ein Slice ist, wird die Next
-Methode nicht einfach als Traversierung von handlers
implementiert, sondern führt einen Verarbeitungsfortschrittsdatensatz index
ein, der auf 0 initialisiert, am Anfang der Methode inkrementiert und nach Abschluss einer Methodenausführung erneut inkrementiert wird.
Das Design von Next
hat einen großen Bezug zu seiner Verwendung, hauptsächlich zur Zusammenarbeit mit einigen Middleware-Funktionen. Wenn beispielsweise während der Ausführung eines bestimmten handler
ein panic
ausgelöst wird, kann der Fehler mit recover
in der Middleware abgefangen werden, und dann kann Next
erneut aufgerufen werden, um die nachfolgenden handlers
weiter auszuführen, ohne das gesamte handlers
-Array aufgrund des Problems eines handler
zu beeinträchtigen.
Handle Panic
In Gin stürzt das gesamte Framework nicht direkt ab, wenn die Verarbeitungsfunktion einer bestimmten Anfrage einen panic
auslöst. Stattdessen wird eine Fehlermeldung ausgegeben und der Dienst weiterhin bereitgestellt. Es ist in etwa so, wie Lua-Frameworks normalerweise xpcall
verwenden, um Nachrichtenverarbeitungsfunktionen auszuführen. Dieser Vorgang ist der Feature-Punkt "Absturzfrei", der in der offiziellen Dokumentation erwähnt wird.
Wie oben erwähnt, wird bei Verwendung von gin.Default
zum Erstellen einer Engine
die Use
-Methode der Engine
ausgeführt, um zwei Funktionen zu importieren. Eine davon ist der Rückgabewert der Recovery
-Funktion, die ein Wrapper anderer Funktionen ist. Die zuletzt aufgerufene Funktion ist CustomRecoveryWithWriter
. Schauen wir uns die Implementierung dieser Funktion an.
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc { //... Anderen Code weglassen return func(c *Context) { defer func() { if err := recover(); err!= nil { //... Fehlerbehandlungscode } }() c.Next() // Führe den nächsten Handler aus } }
Wir konzentrieren uns hier nicht auf die Details der Fehlerbehandlung, sondern schauen uns nur an, was sie tut. Diese Funktion gibt eine anonyme Funktion zurück. In dieser anonymen Funktion wird eine andere anonyme Funktion mit defer
registriert. In dieser inneren anonymen Funktion wird recover
verwendet, um den panic
abzufangen, und dann wird die Fehlerbehandlung durchgeführt. Nach Abschluss der Behandlung wird die Next
-Methode des Context
aufgerufen, sodass die handlers
des Context
, die ursprünglich sequenziell ausgeführt wurden, weiterhin ausgeführt werden können.
Leapcell: Die Next-Gen Serverless-Plattform für Webhosting, asynchrone Aufgaben und Redis
Lass mich abschließend die beste Plattform für die Bereitstellung von Gin-Diensten vorstellen: Leapcell.
1. Mehrsprachige Unterstützung
- Entwickle mit JavaScript, Python, Go oder Rust.
2. Stelle unbegrenzt viele Projekte kostenlos bereit
- Zahle nur für die Nutzung – keine Anfragen, keine Gebühren.
3. Unschlagbare Kosteneffizienz
- Pay-as-you-go ohne Leerlaufgebühren.
- Beispiel: 25 $ unterstützen 6,94 Millionen Anfragen bei einer durchschnittlichen Antwortzeit von 60 ms.
4. Optimierte Entwicklererfahrung
- Intuitive Benutzeroberfläche für mühelose Einrichtung.
- Vollautomatische CI/CD-Pipelines und GitOps-Integration.
- Echtzeit-Metriken und Protokollierung für umsetzbare Erkenntnisse.
5. Mühelose Skalierbarkeit und hohe Leistung
- Automatische Skalierung zur mühelosen Bewältigung hoher Parallelität.
- Null Betriebsaufwand – konzentriere dich einfach auf den Aufbau.
Entdecke mehr in der Dokumentation!
Leapcell Twitter: https://x.com/LeapcellHQ