Gin
Offizielle Dokumentation: Gin Web Framework (gin-gonic.com)
Repository-Adresse: gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang)
Offizielle Beispiele: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)
Einführung
Gin ist ein in Go (Golang) geschriebenes Web-Framework. Es verfügt über eine martini-ähnliche API mit deutlich besserer Leistung – dank httprouter ist es bis zu 40-mal schneller. Wenn Sie Leistung und gute Produktivität benötigen, werden Sie Gin lieben. Im Vergleich zu Iris und Beego ist Gin eher ein leichtgewichtiges Framework, das sich nur um den Web-Teil kümmert und极致 Routing-Leistung anstrebt. Die Funktionen sind vielleicht nicht so umfassend, aber es ist leichtgewichtig und einfach zu erweitern, was auch sein Vorteil ist. Daher ist Gin unter allen Web-Frameworks am einfachsten zu erlernen und zu verwenden.
Eigenschaften
- Schnell: Auf Radix-Baum basierendes Routing, geringer Speicherverbrauch. Keine Reflexion. Vorhersehbare API-Leistung.
- Middleware-Unterstützung: Eingehende HTTP-Anfragen können von einer Reihe von Middlewares und endgültigen Operationen verarbeitet werden. Zum Beispiel: Logger, Authorization, GZIP, endgültige DB-Operation.
- Crash-Behandlung: Gin kann eine Panic abfangen, die während einer HTTP-Anfrage auftritt, und sie recoveren. Somit ist Ihr Server immer verfügbar.
- JSON-Validierung: Gin kann JSON-Anfragen parsen und validieren, zum Beispiel das Vorhandensein erforderlicher Werte überprüfen.
- Routing-Gruppen: Bessere Organisation von Routen. Ob Autorisierung erforderlich ist, verschiedene API-Versionen... Außerdem können diese Gruppen unbegrenzt verschachtelt werden, ohne die Leistung zu beeinträchtigen.
- Fehlerverwaltung: Gin bietet eine bequeme Methode, um alle während einer HTTP-Anfrage aufgetretenen Fehler zu sammeln. Schließlich kann die Middleware sie in eine Protokolldatei, Datenbank schreiben und über das Netzwerk senden.
- Eingebautes Rendering: Gin bietet einfach zu verwendende APIs für JSON-, XML- und HTML-Rendering.
- Erweiterbarkeit: Das Erstellen einer neuen Middleware ist sehr einfach
Installation
Stand 2022/11/22 unterstützt Gin die Go-Mindestversion 1.16, es wird empfohlen, go mod zur Verwaltung von Projektabhängigkeiten zu verwenden.
go get -u github.com/gin-gonic/ginImport
import "github.com/gin-gonic/gin"Schnellstart
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
engine := gin.Default() // Gin-Engine erstellen
engine.GET("/ping", func(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
engine.Run() // Server starten, standardmäßig Überwachung von localhost:8080
}Anfrage-URL
GET localhost:8080/pingAntwort
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 22 Nov 2022 08:47:11 GMT
Content-Length: 18
{
"message": "pong"
}
Response file saved.
> 2022-11-22T164711.200.jsonDokumentation
Eigentlich enthält die offizielle Gin-Dokumentation nicht viele Tutorials, meist nur einige Einführungen und grundlegende Verwendungen sowie einige Beispiele. Aber unter der gin-gonic/-Organisation gibt es ein gin-gonic/examples-Repository, ein von der Community gemeinsam gepflegtes gin-Beispiel-Repository. Alles ist auf Englisch, die Aktualisierungshäufigkeit ist nicht besonders hoch, auch der Autor hat von hier aus langsam das gin-Framework erlernt.
Beispiel-Repository-Adresse: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)
TIP
Vor dem Start wird empfohlen, HttpRouter zu lesen: HttpRouter
Parameter-Parsing
Die Parameter-Parsing in gin unterstützt insgesamt drei Methoden: Routenparameter, URL-Parameter, Formularparameter. Diese werden unten einzeln mit Code-Beispielen erklärt, relativ einfach und verständlich.
Routenparameter
Routenparameter sind tatsächlich eine Kapselung der Parameter-Parsing-Funktionalität von HttpRouter, die Verwendungsmethode ist im Wesentlichen mit HttpRouter identisch.
package main
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)
func main() {
e := gin.Default()
e.GET("/findUser/:username/:userid", FindUser)
e.GET("/downloadFile/*filepath", UserPage)
log.Fatalln(e.Run(":8080"))
}
// Beispiel für benannte Parameter
func FindUser(c *gin.Context) {
username := c.Param("username")
userid := c.Param("userid")
c.String(http.StatusOK, "username is %s\n userid is %s", username, userid)
}
// Beispiel für Pfadparameter
func UserPage(c *gin.Context) {
filepath := c.Param("filepath")
c.String(http.StatusOK, "filepath is %s", filepath)
}Beispiel 1
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'username is jack
userid is 001Beispiel 2
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'filepath is /img/fruit.pngURL-Parameter
Traditionelle URL-Parameter, das Format ist /url?key=val&key1=val1&key2=val2.
package main
import (
"github.com/gin-gonic/gin"
"log"
"net/http"
)
func main() {
e := gin.Default()
e.GET("/findUser", FindUser)
log.Fatalln(e.Run(":8080"))
}
func FindUser(c *gin.Context) {
username := c.DefaultQuery("username", "defaultUser")
userid := c.Query("userid")
c.String(http.StatusOK, "username is %s\nuserid is %s", username, userid)
}Beispiel 1
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'username is jack
userid is 001Beispiel 2
curl --location --request GET '127.0.0.1:8080/findUser'username is defaultUser
userid isFormularparameter
Der Inhaltstyp von Formularen ist normalerweise application/json, application/x-www-form-urlencoded, application/xml, multipart/form-data.
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
e := gin.Default()
e.POST("/register", RegisterUser)
e.POST("/update", UpdateUser)
e.Run(":8080")
}
func RegisterUser(c *gin.Context) {
username := c.PostForm("username")
password := c.PostForm("password")
c.String(http.StatusOK, "successfully registered,your username is [%s],password is [%s]", username, password)
}
func UpdateUser(c *gin.Context) {
var form map[string]string
c.ShouldBind(&form)
c.String(http.StatusOK, "successfully update,your username is [%s],password is [%s]", form["username"], form["password"])
}Beispiel 1: Verwendung von form-data
curl --location --request POST '127.0.0.1:8080/register' \
--form 'username="jack"' \
--form 'password="123456"'successfully registered,your username is [jack],password is [123456]Die PostForm-Methode analysiert standardmäßig Formulare vom Typ application/x-www-form-urlencoded und multipart/form-data.
Beispiel 2: Verwendung von json
curl --location --request POST '127.0.0.1:8080/update' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"username",
"password":"123456"
}'successfully update,your username is [username],password is [123456]Daten-Parsing
In den meisten Fällen verwenden wir Strukturen, um Daten zu speichern, anstatt Parameter direkt zu parsen. In gin sind die Hauptmethoden für die Datenbindung Bind() und ShouldBind(). Der Unterschied zwischen beiden besteht darin, dass erstere intern auch ShouldBind() aufruft, aber bei Rückgabe von err direkt mit 400 antwortet, während letztere dies nicht tut. Wenn Sie flexiblere Fehlerbehandlung wünschen, wird letztere empfohlen. Diese beiden Funktionen schließen automatisch anhand des content-type der Anfrage darauf, wie sie geparst werden soll.
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
// Ruft ShouldBindWith() auf
if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // Direkte 400 BadRequest-Antwort
return err
}
return nil
}Wenn Sie selbst auswählen möchten, können Sie BindWith() und ShouldBindWith() verwenden, zum Beispiel:
c.MustBindWith(obj, binding.JSON) //json
c.MustBindWith(obj, binding.XML) //xmlGin unterstützt folgende Bindungstypen:
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{}
Query = queryBinding{}
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
TOML = tomlBinding{}
)Beispiel
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
type LoginUser struct {
Username string `binding:"required" json:"username" form:"username" uri:"username"`
Password string `binding:"required" json:"password" form:"password" uri:"password"`
}
func main() {
e := gin.Default()
e.POST("/loginWithJSON", Login)
e.POST("/loginWithForm", Login)
e.GET("/loginWithQuery/:username/:password", Login)
e.Run(":8080")
}
func Login(c *gin.Context) {
var login LoginUser
// ShouldBind verwenden, damit gin automatisch ableitet
if c.ShouldBind(&login) == nil && login.Password != "" && login.Username != "" {
c.String(http.StatusOK, "login successfully !")
} else {
c.String(http.StatusBadRequest, "login failed !")
}
fmt.Println(login)
}JSON-Datenbindung
curl --location --request POST '127.0.0.1:8080/loginWithJSON' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"root",
"password":"root"
}'login successfully !Formular-Datenbindung
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'login successfully !URL-Datenbindung
curl --location --request GET '127.0.0.1:8080/loginWithQuery/root/root'login failed !Hier tritt ein Fehler auf, da der ausgegebene content-type ein leerer String ist und nicht abgeleitet werden kann, wie die Daten geparst werden sollen. Wenn Sie also URL-Parameter verwenden, sollten Sie die Parsing-Methode manuell angeben, zum Beispiel:
if err := c.ShouldBindUri(&login); err == nil && login.Password != "" && login.Username != "" {
c.String(http.StatusOK, "login successfully !")
} else {
fmt.Println(err)
c.String(http.StatusBadRequest, "login failed !")
}Mehrfaches Binden
Allgemeine Methoden binden Daten durch Aufrufen der c.Request.Body-Methode, aber diese Methode kann nicht mehrmals aufgerufen werden, zum Beispiel c.ShouldBind, nicht wiederverwendbar. Wenn Sie mehrmals binden möchten, können Sie c.ShouldBindBodyWith verwenden.
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// Liest c.Request.Body und speichert das Ergebnis im Kontext.
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// Hier wird der im Kontext gespeicherte Body wiederverwendet.
}
if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `the body should be formB JSON`)
// Kann auch andere Formate akzeptieren
}
if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
c.String(http.StatusOK, `the body should be formB XML`)
}
}TIP
c.ShouldBindBodyWith speichert den Body vor dem Binden im Kontext. Dies hat leichte Auswirkungen auf die Leistung. Wenn ein Aufruf zum Binden ausreicht, sollten Sie diese Methode nicht verwenden. Nur bestimmte Formate benötigen diese Funktionalität, wie JSON, XML, MsgPack, ProtoBuf. Für andere Formate wie Query, Form, FormPost, FormMultipart kann c.ShouldBind() mehrmals aufgerufen werden, ohne Leistungseinbußen.
Datenvalidierung
Das in gin eingebaute Validierungstool ist tatsächlich github.com/go-playground/validator/v10, die Verwendung ist fast identisch, Validator
Einfaches Beispiel
type LoginUser struct {
Username string `binding:"required" json:"username" form:"username" uri:"username"`
Password string `binding:"required" json:"password" form:"password" uri:"password"`
}
func main() {
e := gin.Default()
e.POST("/register", Register)
log.Fatalln(e.Run(":8080"))
}
func Register(ctx *gin.Context) {
newUser := &LoginUser{}
if err := ctx.ShouldBind(newUser); err == nil {
ctx.String(http.StatusOK, "user%+v", *newUser)
} else {
ctx.String(http.StatusBadRequest, "invalid user,%v", err)
}
}Test
curl --location --request POST 'http://localhost:8080/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"jack1"
}'Ausgabe
invalid user,Key: 'LoginUser.Password' Error:Field validation for 'Password' failed on the 'required' tagTIP
Zu beachten ist, dass in gin das Validierungs-Tag des Validators binding ist, während bei alleiniger Verwendung von validator das Validierungs-Tag validator ist
Datenantwort
Die Datenantwort ist der letzte Schritt bei der Verarbeitung einer Schnittstelle. Das Backend gibt alle verarbeiteten Daten über das HTTP-Protokoll an den Aufrufer zurück. Gin bietet umfangreiche eingebaute Unterstützung für Datenantworten, die Verwendung ist einfach und klar, sehr einfach zu erlernen.
Einfaches Beispiel
func Hello(c *gin.Context) {
// Gibt Daten im reinen String-Format zurück, http.StatusOK repräsentiert den 200-Statuscode, Daten sind "Hello world !"
c.String(http.StatusOK, "Hello world !")
}HTML-Rendering
TIP
Beim Laden von Dateien ist der Standard-Stammpfad der Projektpfad, also der Pfad, in dem sich die go.mod-Datei befindet. Die index.html im folgenden Beispiel befindet sich unter index.html im Stammpfad. Normalerweise werden diese Template-Dateien nicht im Stammpfad gespeichert, sondern in einem statischen Ressourcenordner
func main() {
e := gin.Default()
// HTML-Datei laden, kann auch Engine.LoadHTMLGlob() verwenden
e.LoadHTMLFiles("index.html")
e.GET("/", Index)
log.Fatalln(e.Run(":8080"))
}
func Index(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{})
}Test
curl --location --request GET 'http://localhost:8080/'Rückgabe
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>GinLearn</title>
</head>
<body>
<h1>Hello World!</h1>
<h1>This is a HTML Template Render Example</h1>
</body>
</html>Schnelle Antwort
Zuvor wurde häufig die context.String()-Methode zur Datenantwort verwendet. Dies ist die ursprünglichste Antwortmethode, die direkt einen String zurückgibt. In gin sind tatsächlich viele schnelle Antwortmethoden eingebaut, zum Beispiel:
// Verwendet Render zum Schreiben des Response-Headers und zum Rendern der Daten
func (c *Context) Render(code int, r render.Render)
// Rendert ein HTML-Template, name ist der HTML-Pfad, obj ist der Inhalt
func (c *Context) HTML(code int, name string, obj any)
// Rendert Daten als eingerückten JSON-String, wird normalerweise nicht empfohlen, da dies mehr Übertragungsaufwand verursacht
func (c *Context) IndentedJSON(code int, obj any)
// Sicheres JSON, kann JSON-Hijacking verhindern, Details unter: https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)
// Rendert im JSONP-Verfahren
func (c *Context) JSONP(code int, obj any)
// Rendert im JSON-Verfahren
func (c *Context) JSON(code int, obj any)
// Rendert im JSON-Verfahren, konvertiert Unicode-Code in ASCII-Code
func (c *Context) AsciiJSON(code int, obj any)
// Rendert im JSON-Verfahren, ohne Escape von HTML-Sonderzeichen
func (c *Context) PureJSON(code int, obj any)
// Rendert im XML-Verfahren
func (c *Context) XML(code int, obj any)
// Rendert im YML-Verfahren
func (c *Context) YAML(code int, obj any)
// Rendert im TOML-Verfahren
func (c *Context) TOML(code int, obj interface{})
// Rendert im ProtoBuf-Verfahren
func (c *Context) ProtoBuf(code int, obj any)
// Rendert im String-Verfahren
func (c *Context) String(code int, format string, values ...any)
// Leitet zu einem bestimmten Ort um
func (c *Context) Redirect(code int, location string)
// Schreibt Daten in den Antwort-Stream
func (c *Context) Data(code int, contentType string, data []byte)
// Liest Stream über Reader und schreibt in den Antwort-Stream
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)
// Schreibt Datei effizient in den Antwort-Stream
func (c *Context) File(filepath string)
// Schreibt Datei-Stream aus fs auf effiziente Weise in den Antwort-Stream
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)
// Schreibt Datei-Stream aus fs auf effiziente Weise in den Antwort-Stream und wird beim Client mit dem angegebenen Dateinamen heruntergeladen
func (c *Context) FileAttachment(filepath, filename string)
// Schreibt Server-Push-Stream in den Antwort-Stream
func (c *Context) SSEvent(name string, message any)
// Sendet eine Stream-Antwort und gibt einen booleschen Wert zurück, um zu bestimmen, ob der Client den Stream unterbrochen hat
func (c *Context) Stream(step func(w io.Writer) bool) boolFür die meisten Anwendungen wird am häufigsten context.JSON verwendet, die anderen seltener. Hier werden keine Beispiele gezeigt, da alle relativ einfach und verständlich sind, fast alles ist direkter Aufruf.
Asynchrone Verarbeitung
In gin muss die asynchrone Verarbeitung in Kombination mit Goroutines verwendet werden, die Anwendung ist sehr einfach.
// copy gibt eine Kopie des aktuellen Context zurück, um sicher außerhalb des aktuellen Context-Gültigkeitsbereichs verwendet zu werden, kann an eine Goroutine übergeben werden
func (c *Context) Copy() *Contextfunc main() {
e := gin.Default()
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}
func Hello(c *gin.Context) {
ctx := c.Copy()
go func() {
// Sub-Routine sollte die Kopie des Context verwenden, nicht den ursprünglichen Context
log.Println("Asynchrone Verarbeitungsfunktion: ", ctx.HandlerNames())
}()
log.Println("Schnittstellen-Verarbeitungsfunktion: ", c.HandlerNames())
c.String(http.StatusOK, "hello")
}Test
curl --location --request GET 'http://localhost:8080/hello'Ausgabe
2022/12/21 13:33:47 Asynchrone Verarbeitungsfunktion: []
2022/12/21 13:33:47 Schnittstellen-Verarbeitungsfunktion: [github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.Hello]
[GIN] 2022/12/21 - 13:33:47 | 200 | 11.1927ms | ::1 | GET "/hello"Wie Sie sehen, ist die Ausgabe unterschiedlich. Beim Kopieren wurden aus Sicherheitsgründen viele Elementwerte gelöscht.
Dateiübertragung
Die Dateiübertragung ist eine unverzichtbare Funktion von Web-Anwendungen. Gin unterstützt dies mit einer sehr einfachen Kapselung, aber im Wesentlichen ist der Prozess derselbe wie bei der Verwendung der nativen net/http. Der Prozess besteht darin, den Dateistream aus dem Anfragekörper zu lesen und dann lokal zu speichern.
Einzeldatei-Upload
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
log.Fatalln(e.Run(":8080"))
}
func uploadFile(ctx *gin.Context) {
// Datei abrufen
file, err := ctx.FormFile("file")
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// Lokal speichern
err = ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// Ergebnis zurückgeben
ctx.String(http.StatusOK, "upload %s size:%d byte successfully!", file.Filename, file.Size)
}Test
curl --location --request POST 'http://localhost:8080/upload' \
--form 'file=@"/C:/Users/user/Pictures/Camera Roll/a.jpg"'Ergebnis
upload a.jpg size:1424 byte successfully!TIP
Normalerweise wird für Datei-Uploads POST als Method angegeben. Einige Unternehmen bevorzugen möglicherweise PUT. Ersteres ist eine einfache HTTP-Anfrage, letzteres eine komplexe HTTP-Anfrage. Der genaue Unterschied wird hier nicht erläutert. Wenn Sie letzteres verwenden, insbesondere bei Projekten mit getrenntem Frontend und Backend, ist eine entsprechende CORS-Konfiguration erforderlich, und Gin unterstützt standardmäßig kein CORS CORS-Konfiguration.
Mehrfachdatei-Upload
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
e.POST("/uploadFiles", uploadFiles)
log.Fatalln(e.Run(":8080"))
}
func uploadFiles(ctx *gin.Context) {
// Von gin geparstes Multipart-Formular abrufen
form, _ := ctx.MultipartForm()
// Dateiliste basierend auf Schlüsselwert abrufen
files := form.File["files"]
// Dateiliste durchlaufen und lokal speichern
for _, file := range files {
err := ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "upload failed")
return
}
}
// Ergebnis zurückgeben
ctx.String(http.StatusOK, "upload %d files successfully!", len(files))
}Test
curl --location --request POST 'http://localhost:8080/uploadFiles' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/a.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/123.jpg"' \
--form 'files=@"/C:/Users/Stranger/Pictures/Camera Roll/girl.jpg"'Ausgabe
upload 3 files successfully!Datei-Download
Für den Datei-Download hat Gin die API der ursprünglichen Standardbibliothek erneut gekapselt, was den Datei-Download extrem einfach macht.
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
e.POST("/uploadFiles", uploadFiles)
e.GET("/download/:filename", download)
log.Fatalln(e.Run(":8080"))
}
func download(ctx *gin.Context) {
// Dateinamen abrufen
filename := ctx.Param("filename")
// Entsprechende Datei zurückgeben
ctx.FileAttachment(filename, filename)
}Test
curl --location --request GET 'http://localhost:8080/download/a.jpg'Ergebnis
Content-Disposition: attachment; filename="a.jpg"
Date: Wed, 21 Dec 2022 08:04:17 GMT
Last-Modified: Wed, 21 Dec 2022 07:50:44 GMTFinden Sie es nicht zu einfach? Versuchen wir, den Prozess ohne die Framework-Methoden selbst zu schreiben
func download(ctx *gin.Context) {
// Parameter abrufen
filename := ctx.Param("filename")
// Anfrage-Antwort-Objekt und Anfrage-Objekt
response, request := ctx.Writer, ctx.Request
// In Response-Header schreiben
// response.Header().Set("Content-Type", "application/octet-stream") Datei als Binärstream übertragen
response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // Sicheres Escaping des Dateinamens
response.Header().Set("Content-Transfer-Encoding", "binary") // Übertragungskodierung
http.ServeFile(response, request, filename)
}Tatsächlich hat auch net/http bereits gut genug gekapselt
TIP
Sie können Engine.MaxMultipartMemory verwenden, um den maximalen Speicher für Dateiübertragungen festzulegen, standardmäßig 32 << 20 // 32 MB
Routing-Verwaltung
Die Routing-Verwaltung ist ein sehr wichtiger Teil eines Systems, es muss sichergestellt werden, dass jede Anfrage korrekt der entsprechenden Funktion zugeordnet wird.
Routing-Gruppen
Das Erstellen einer Routing-Gruppe dient dazu, Schnittstellen zu kategorisieren. Verschiedene Kategorien von Schnittstellen entsprechen verschiedenen Funktionen und sind einfacher zu verwalten.
func Hello(c *gin.Context) {
}
func Login(c *gin.Context) {
}
func Update(c *gin.Context) {
}
func Delete(c *gin.Context) {
}Angenommen, wir haben die oben genannten vier Schnittstellen, vorerst unabhängig von der internen Implementierung. Hello, Login sind eine Gruppe, Update, Delete sind eine Gruppe.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroupBeim Erstellen einer Gruppe können wir auch Handler für das Stammverzeichnis der Gruppe registrieren, aber meistens wird dies nicht getan.
func main() {
e := gin.Default()
v1 := e.Group("v1")
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("v2")
{
v2.POST("/update", Update)
v2.DELETE("/delete", Delete)
}
}Wir haben sie in v1, v2 zwei Gruppen unterteilt. Die geschweiften Klammern {} dienen nur der Übersichtlichkeit und zeigen an, dass die in den Klammern registrierten Handler zur selben Routing-Gruppe gehören. Funktionell haben sie keine Wirkung. Ebenso unterstützt gin auch verschachtelte Gruppen, die Methode ist identisch mit dem obigen Beispiel, wird hier nicht demonstriert.
404-Routing
Die Engine-Struktur in gin bietet eine Methode NoRoute, um festzulegen, wie verarbeitet werden soll, wenn die aufgerufene URL nicht existiert. Entwickler können die Logik in diese Methode schreiben, damit sie automatisch aufgerufen wird, wenn ein Router nicht gefunden wird. Standardmäßig wird der Statuscode 404 zurückgegeben.
func (engine *Engine) NoRoute(handlers ...HandlerFunc)Wir nehmen das vorherige Beispiel
func main() {
e := gin.Default()
v1 := e.Group("v1")
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("v2")
{
v2.POST("/update", Update)
v2.DELETE("/delete", Delete)
}
// Handler registrieren
e.NoRoute(func(context *gin.Context) { // Hier nur Demonstration, in Produktionsumgebungen keinen HTML-Code direkt zurückgeben
context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
})
log.Fatalln(e.Run(":8080"))
}Zufällige Anfrage senden
curl --location --request GET 'http://localhost:8080/'<h1>404 Page Not Found</h1>405-Routing
Im HTTP-Statuscode repräsentiert 405, dass die aktuelle Anfragemethode nicht erlaubt ist. Gin bietet folgende Methode
func (engine *Engine) NoMethod(handlers ...HandlerFunc)um einen Handler zu registrieren, der automatisch aufgerufen wird, wenn dies eintritt. Voraussetzung ist, dass Engine.HandleMethodNotAllowed = true gesetzt ist.
func main() {
e := gin.Default()
// Muss auf true gesetzt werden
e.HandleMethodNotAllowed = true
v1 := e.Group("/v1")
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
v2.POST("/update", Update)
v2.DELETE("/delete", Delete)
}
e.NoRoute(func(context *gin.Context) {
context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
})
// Handler registrieren
e.NoMethod(func(context *gin.Context) {
context.String(http.StatusMethodNotAllowed, "method not allowed")
})
log.Fatalln(e.Run(":8080"))
}Nach der Konfiguration unterstützt gin standardmäßig keine OPTION-Anfragen im Header. Testen wir
curl --location --request OPTIONS 'http://localhost:8080/v2/delete'method not allowedDamit ist die Konfiguration erfolgreich
Umleitung
Die Umleitung in gin ist sehr einfach, rufen Sie einfach die gin.Context.Redirect()-Methode auf.
func main() {
e := gin.Default()
e.GET("/", Index)
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}
func Index(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/hello")
}
func Hello(c *gin.Context) {
c.String(http.StatusOK, "hello")
}Test
curl --location --request GET 'http://localhost:8080/'Ausgabe
helloMiddleware
Gin ist sehr leicht und flexibel, sehr erweiterbar, und die Unterstützung für Middleware ist ebenfalls sehr freundlich. In Gin müssen alle Schnittstellenanfragen durch Middleware gehen. Durch Middleware können Entwickler viele Funktionen und Logiken selbst implementieren. Gin hat zwar selbst wenige Funktionen, aber die von der Drittanbieter-Community entwickelten Gin-Erweiterungs-Middlewares sind sehr umfangreich.
Middleware ist im Wesentlichen immer noch ein Schnittstellen-Handler
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)In gewissem Sinne ist auch jeder Handler, der einer Anfrage entspricht, eine Middleware, nur mit einem sehr kleinen lokalen Geltungsbereich.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}In der Quellcode von gin verwendet das im Default-Funktion zurückgegebene Standard-Engine zwei Standard-Middlewares Logger(), Recovery(). Wenn Sie die Standard-Middlewares nicht verwenden möchten, können Sie gin.New() verwenden.
Globale Middleware
Globale Middleware hat einen globalen Geltungsbereich, alle Anfragen im gesamten System durchlaufen diese Middleware.
func GlobalMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("Globale Middleware ausgeführt...")
}
}Erstellen Sie zuerst eine Closure-Funktion, um die Middleware zu erstellen, und registrieren Sie dann die globale Middleware über Engine.Use().
func main() {
e := gin.Default()
// Globale Middleware registrieren
e.Use(GlobalMiddleware())
v1 := e.Group("/v1")
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
v2.POST("/update", Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}Test
curl --location --request GET 'http://localhost:8080/v1/hello'Ausgabe
[GIN-debug] Listening and serving HTTP on :8080
Globale Middleware ausgeführt...
[GIN] 2022/12/21 - 11:57:52 | 200 | 538.9µs | ::1 | GET "/v1/hello"Lokale Middleware
Lokale Middleware hat einen lokalen Geltungsbereich, lokale Anfragen im System durchlaufen diese Middleware. Lokale Middleware kann auf einer einzelnen Route registriert werden, meistens wird sie jedoch auf einer Routing-Gruppe registriert.
func main() {
e := gin.Default()
// Globale Middleware registrieren
e.Use(GlobalMiddleware())
// Routing-Gruppen lokale Middleware registrieren
v1 := e.Group("/v1", LocalMiddleware())
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
// Einzelne Route lokale Middleware registrieren
v2.POST("/update", LocalMiddleware(), Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}Test
curl --location --request POST 'http://localhost:8080/v2/update'Ausgabe
Globale Middleware ausgeführt...
Lokale Middleware ausgeführt
[GIN] 2022/12/21 - 12:05:03 | 200 | 999.9µs | ::1 | POST "/v2/update"Middleware-Prinzip
Die Verwendung und Anpassung von Middleware in Gin ist sehr einfach, die internen Prinzipien sind ebenfalls relativ einfach. Für das weitere Lernen ist ein einfaches Verständnis der internen Prinzipien erforderlich. Gin's Middleware verwendet das Verantwortlichkeitskettenmuster (Chain of Responsibility). Context verwaltet eine HandlersChain, im Wesentlichen ein []HandlerFunc, und einen index vom Datentyp int8. In der Engine.handlerHTTPRequest(c *Context)-Methode zeigt ein Codeabschnitt den Aufrufprozess: Nachdem gin die entsprechende Route im Routing-Baum gefunden hat, ruft es die Next()-Methode auf.
if value.handlers != nil {
// Aufrufkette dem Context zuweisen
c.handlers = value.handlers
c.fullPath = value.fullPath
// Middleware aufrufen
c.Next()
c.writermem.WriteHeaderNow()
return
}Der Aufruf von Next() ist der Schlüssel. Next() durchläuft die HandlerFunc in handlers der Route und führt sie aus. Hier sehen Sie, dass index dazu dient, die Aufrufposition der Middleware aufzuzeichnen. Dabei ist auch die für die Route registrierte Schnittstellenfunktion in handlers enthalten, weshalb wir zuvor gesagt haben, dass auch eine Schnittstelle eine Middleware ist.
func (c *Context) Next() {
// Bei Eintritt +1, um endlose Rekursion zu vermeiden, Standardwert ist -1
c.index++
for c.index < int8(len(c.handlers)) {
// HandlerFunc ausführen
c.handlers[c.index](c)
// Ausführung abgeschlossen, index+1
c.index++
}
}Ändern wir die Logik von Hello(), um zu überprüfen, ob dies wirklich so ist
func Hello(c *gin.Context) {
fmt.Println(c.HandlerNames())
}Ausgabe
[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.GlobalMiddleware.func1 main.LocalMiddleware.func1 main.Hello]Wie Sie sehen, ist die Reihenfolge der Middleware-Aufrufkette: Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello. Das letzte Element der Aufrufkette ist die tatsächlich auszuführende Schnittstellenfunktion, alle vorherigen sind Middlewares.
TIP
Bei der Registrierung lokaler Routen gibt es folgende Assertion
finalSize := len(group.Handlers) + len(handlers) // Gesamtzahl der Middlewares
assert1(finalSize < int(abortIndex), "too many handlers")Dabei ist abortIndex int8 = math.MaxInt8 >> 1 der Wert 63, d.h. die Anzahl der Routing-Registrierungen im System sollte 63 nicht überschreiten.
Timer-Middleware
Nachdem Sie die oben genannten Middleware-Prinzipien kennen, können Sie eine einfache Anfragezeit-Statistik-Middleware schreiben.
func TimeMiddleware() gin.HandlerFunc {
return func(context *gin.Context) {
// Startzeit aufzeichnen
start := time.Now()
// Nachfolgende Aufrufkette ausführen
context.Next()
// Zeitintervall berechnen
duration := time.Since(start)
// Nanosekunden ausgeben, um das Ergebnis zu beobachten
fmt.Println("Anfragezeit: ", duration.Nanoseconds())
}
}
func main() {
e := gin.Default()
// Globale Middleware registrieren, Timer-Middleware
e.Use(GlobalMiddleware(), TimeMiddleware())
// Routing-Gruppen lokale Middleware registrieren
v1 := e.Group("/v1", LocalMiddleware())
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
// Einzelne Route lokale Middleware registrieren
v2.POST("/update", LocalMiddleware(), Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}Test
curl --location --request GET 'http://localhost:8080/v1/hello'Ausgabe
Anfragezeit: 517600Eine einfache Timer-Middleware wurde bereits fertig geschrieben. Später können Sie durch eigene Erkundung einige praktischere Middlewares schreiben.
Server-Konfiguration
Die Verwendung der Standardkonfiguration allein reicht bei weitem nicht aus. In den meisten Fällen müssen viele Server-Konfigurationen geändert werden, um die Anforderungen zu erfüllen.
HTTP-Konfiguration
Sie können Server über net/http erstellen und konfigurieren. Gin selbst unterstützt auch die Verwendung von Gin wie die native API.
func main() {
router := gin.Default()
server := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
log.Fatal(server.ListenAndServe())
}Statische Ressourcen-Konfiguration
Statische Ressourcen waren früher ein unverzichtbarer Teil des Servers. Obwohl der Anteil jetzt allmählich abnimmt, gibt es immer noch viele Systeme, die eine monolithische Architektur verwenden.
Gin bietet drei Methoden zum Laden statischer Ressourcen
// Lädt einen bestimmten statischen Ordner
func (group *RouterGroup) Static(relativePath, root string) IRoutes
// Lädt ein bestimmtes fs
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes
// Lädt eine bestimmte statische Datei
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutesTIP
relativePath ist der relative Pfad, der auf die Web-URL abgebildet wird, root ist der tatsächliche Pfad der Datei im Projekt
Angenommen, die Projektstruktur sieht wie folgt aus
root
|
|-- static
| |
| |-- a.jpg
| |
| |-- favicon.ico
|
|-- view
|
|-- htmlfunc main() {
router := gin.Default()
// Statisches Dateiverzeichnis laden
router.Static("/static", "./static")
// Statisches Dateiverzeichnis laden
router.StaticFS("/view", http.Dir("view"))
// Statische Datei laden
router.StaticFile("/favicon", "./static/favicon.ico")
router.Run(":8080")
}CORS-Konfiguration
Gin selbst hat keine CORS-Konfiguration verarbeitet. Sie müssen selbst Middleware schreiben, um die entsprechenden Anforderungen zu implementieren. Das ist auch nicht schwierig. Jeder, der mit dem HTTP-Protokoll etwas vertraut ist, kann es schreiben. Die Logik ist fast immer dieselbe.
func CorsMiddle() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
// In Produktionsumgebungen wird normalerweise nicht * ausgefüllt, sondern die angegebene Domain
c.Header("Access-Control-Allow-Origin", origin)
// Erlaubte HTTP METHOD
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
// Erlaubte Anfrage-Header
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
// Erlaubte Antwort-Header für Client-Zugriff
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
// Ob Authentifizierungsinformationen Credentials benötigt werden, dies können Cookies, Authorization-Header oder TLS-Client-Zertifikate sein
// Wenn auf true gesetzt, kann Access-Control-Allow-Origin nicht * sein
c.Header("Access-Control-Allow-Credentials", "true")
}
// OPTION-Anfragen durchlassen, aber nachfolgende Methoden nicht ausführen
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
// Durchlassen
c.Next()
}
}Registrieren Sie die Middleware als globale Middleware
Sitzungssteuerung
In der heutigen Zeit gibt es drei beliebte Methoden zur Web-Sitzungssteuerung: Cookie, Session, JWT.
Cookie
Die Informationen in Cookies werden als Schlüssel-Wert-Paare im Browser gespeichert, und die Daten können direkt im Browser eingesehen werden
Vorteile:
- Einfache Struktur
- Persistente Daten
Nachteile:
- Größenbeschränkung
- Klartextspeicherung
- Anfällig für CSRF-Angriffe
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
// Entsprechendes Cookie abrufen
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
// Cookie setzen Parameter: key, val, Lebensdauer, Pfad, Domain, ob anderen den Zugriff via JS erlaubt ist, nur http
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run()
}Einfache Cookies wurden vor fünf oder sechs Jahren häufiger verwendet, aber der Autor verwendet selten nur Cookies zur Sitzungssteuerung, da dies tatsächlich nicht sehr sicher ist.
Session
Sessions werden auf dem Server gespeichert, dann wird ein Cookie an den Browser gesendet. Im Cookie wird die session_id gespeichert. Bei jeder nachfolgenden Anfrage kann der Server über die session_id die entsprechenden Session-Informationen abrufen
Vorteile:
- Speicherung auf dem Server, erhöht die Sicherheit, erleichtert die Verwaltung
Nachteile:
- Speicherung auf dem Server, erhöht den Server-Overhead, verringert die Leistung
- Basiert auf Cookie-Erkennung, unsicher
- Authentifizierungsinformationen sind in verteilten Umgebungen nicht synchronisiert
Session und Cookie sind untrennbar miteinander verbunden. Wann immer Session verwendet wird, wird standardmäßig auch Cookie verwendet. Gin unterstützt standardmäßig kein Session, da Cookie Teil des HTTP-Protokolls ist, Session aber nicht. Es gibt jedoch Drittanbieter-Middleware-Unterstützung. Installieren Sie einfach die Abhängigkeit: gin-contrib/sessions: Gin middleware for session management (github.com)
go get github.com/gin-contrib/sessionsUnterstützt Cookie, Redis, MongoDB, GORM, PostgreSQL
func main() {
r := gin.Default()
// Cookie-basierte Storage-Engine erstellen
store := cookie.NewStore([]byte("secret"))
// Session-Middleware setzen, mysession ist der Session-Name und auch der Cookie-Name
r.Use(sessions.Sessions("mysession", store))
r.GET("/incr", func(c *gin.Context) {
// Session initialisieren
session := sessions.Default(c)
var count int
// Wert abrufen
v := session.Get("count")
if v == nil {
count = 0
} else {
count = v.(int)
count++
}
// Setzen
session.Set("count", count)
// Speichern
session.Save()
c.JSON(200, gin.H{"count": count})
})
r.Run(":8000")
}Es wird allgemein nicht empfohlen, Session über Cookie zu speichern. Redis wird empfohlen. Weitere Beispiele finden Sie im offiziellen Repository.
JWT
Vorteile:
- Basiert auf JSON, mehrsprachig universell
- Kann nicht-sensible Informationen speichern
- Sehr kompakt, einfach zu übertragen
- Server muss nicht speichern, vorteilhaft für verteilte Erweiterung
Nachteile:
- Token-Aktualisierungsproblem
- Einmal ausgestellt, kann nicht aktiv kontrolliert werden
Seit der Frontend-Revolution ist der Frontend-Programmierer nicht mehr nur ein "Seiten-Schreiber". Der Trend zur Trennung von Frontend und Backend wird immer stärker. JWT ist am besten geeignet für Frontend-Backend-Trennung und verteilte Systeme zur Sitzungssteuerung und hat große natürliche Vorteile. Da JWT vollständig vom Gin-Inhalt getrennt ist und keine Middleware-Unterstützung hat – weil JWT selbst nicht auf ein Framework oder eine Sprache beschränkt ist – wird hier nicht im Detail darauf eingegangen. Siehe anderes Dokument: JWT
Protokollverwaltung
Die von Gin standardmäßig verwendete Protokoll-Middleware verwendet os.Stdout mit nur den grundlegendsten Funktionen. Gin konzentriert sich nur auf Web-Services. In den meisten Fällen sollten Sie ein ausgereifteres Protokoll-Framework verwenden. Dies ist jedoch nicht Gegenstand dieses Kapitels, und Gin ist sehr erweiterbar und kann leicht andere Frameworks integrieren. Hier wird nur der eingebaute Protokoll-Service besprochen.
Konsolenfarbe
gin.DisableConsoleColor() // Konsolen-Protokollfarbe deaktivierenAußer während der Entwicklung wird in den meisten Fällen nicht empfohlen, dies zu aktivieren
Protokoll in Datei schreiben
func main() {
e := gin.Default()
// Konsolenfarbe ausschalten
gin.DisableConsoleColor()
// Zwei Protokolldateien erstellen
log1, _ := os.Create("info1.log")
log2, _ := os.Create("info2.log")
// Gleichzeitig in zwei Protokolldateien schreiben
gin.DefaultWriter = io.MultiWriter(log1, log2)
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}Das eingebaute Protokoll von gin unterstützt das Schreiben in mehrere Dateien, aber der Inhalt ist identisch, was nicht sehr praktisch ist. Außerdem werden Anfrage-Protokolle nicht in Dateien geschrieben.
func main() {
router := gin.New()
// LoggerWithFormatter Middleware schreibt Protokolle in gin.DefaultWriter
// Standardmäßig gin.DefaultWriter = os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
//TODO Logik zum Schreiben in entsprechende Datei
......
// Benutzerdefiniertes Format ausgeben
return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
param.ClientIP,
param.TimeStamp.Format(time.RFC1123),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}))
router.Use(gin.Recovery())
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}Durch benutzerdefinierte Middleware kann das Protokoll in Dateien geschrieben werden
Routing-Debug-Protokollformat
Hier wird nur das Protokoll geändert, das beim Start die Routing-Informationen ausgibt
func main() {
e := gin.Default()
gin.SetMode(gin.DebugMode)
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
if gin.IsDebugging() {
log.Printf("Route %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
}
}
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}Ausgabe
2022/12/21 17:19:13 Route GET /hello main.Hello 3Fazit: Gin ist eines der am einfachsten zu erlernenden Web-Frameworks in Go. Denn Gin hat es wirklich geschafft, die Verantwortung zu minimieren und sich nur um Web-Services zu kümmern. Andere Authentifizierungslogik, Daten-Caching und andere Funktionen werden den Entwicklern überlassen. Im Vergleich zu diesen großen und umfassenden Frameworks ist das leichtgewichtige und einfache Gin für Anfänger geeigneter und sollte auch erlernt werden. Denn Gin erzwingt nicht die Verwendung einer bestimmten Spezifikation. Wie das Projekt aufgebaut sein soll und welche Struktur verwendet werden soll, muss selbst entschieden werden. Für Anfänger ist dies besser für die Ausbildung der Fähigkeiten.
