Gin
Documentation officielle : Gin Web Framework (gin-gonic.com)
Adresse du dépôt : gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang)
Exemples officiels : gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)
Introduction
Gin est un framework Web écrit en Go (Golang). Il a une API similaire à Martini, avec de bien meilleures performances, grâce à httprouter, jusqu'à 40 fois plus rapide. Si vous avez besoin de performance et d'une bonne productivité, vous allez adorer Gin. Comparé à Iris et Beego, Gin est un framework plus léger, se concentrant uniquement sur la partie Web, poursuivant une performance de routage extrême. Ses fonctionnalités sont peut-être moins complètes, mais il est léger et facile à étendre, ce qui est aussi son avantage. Par conséquent, parmi tous les frameworks Web, Gin est le plus facile à apprendre et à maîtriser.
Caractéristiques
- Rapide : Routage basé sur l'arbre Radix, faible empreinte mémoire. Pas de réflexion. Performance d'API prévisible.
- Support des middlewares : Les requêtes HTTP entrantes peuvent être traitées par une série de middlewares et l'opération finale. Par exemple : Logger, Authorization, GZIP, opération finale DB.
- Gestion des crashes : Gin peut capturer un panic survenant dans une requête HTTP et le récupérer. Ainsi, votre serveur sera toujours disponible.
- Validation JSON : Gin peut analyser et valider le JSON de la requête, par exemple vérifier la présence de valeurs requises.
- Groupes de routes : Mieux organiser les routes. Besoin d'autorisation, différentes versions d'API... De plus, ces groupes peuvent être imbriqués sans limite sans dégrader les performances.
- Gestion des erreurs : Gin fournit un moyen pratique de collecter toutes les erreurs survenues pendant la requête HTTP. Finalement, le middleware peut les écrire dans un fichier de log, une base de données et les envoyer via le réseau.
- Rendu intégré : Gin fournit une API facile à utiliser pour le rendu JSON, XML et HTML.
- Extensibilité : Créer un nouveau middleware est très simple.
Installation
À ce jour 2022/11/22, gin supporte une version minimale de Go 1.16, il est recommandé d'utiliser go mod pour gérer les dépendances du projet.
go get -u github.com/gin-gonic/ginImport
import "github.com/gin-gonic/gin"Démarrage rapide
package main
import (
"github.com/gin-gonic/gin"
"net/http"
)
func main() {
engine := gin.Default() // Créer le moteur gin
engine.GET("/ping", func(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
engine.Run() // Démarrer le serveur, écoute par défaut localhost:8080
}URL de requête
GET localhost:8080/pingRetour
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.jsonDocumentation
En fait, la documentation officielle de Gin ne contient pas beaucoup de tutoriels, la plupart sont juste des introductions, des utilisations de base et quelques exemples. Mais sous l'organisation gin-gonic/, il y a un dépôt gin-gonic/examples, qui est un dépôt d'exemples gin maintenu par la communauté. Tout est en anglais, les mises à jour ne sont pas très fréquentes, c'est aussi de là que j'ai progressivement appris le framework gin.
Adresse du dépôt d'exemples : gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)
TIP
Avant de commencer, il est recommandé de lire HttpRouter : HttpRouter
Analyse des paramètres
L'analyse des paramètres dans gin supporte trois méthodes : paramètres de route, paramètres URL, paramètres de formulaire. Nous allons les expliquer une par une avec des exemples de code, c'est assez simple et facile à comprendre.
Paramètres de route
Les paramètres de route sont en fait une encapsulation de la fonctionnalité d'analyse des paramètres de HttpRouter, la méthode d'utilisation est fondamentalement la même que HttpRouter.
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"))
}
// Exemple de paramètre nommé
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)
}
// Exemple de paramètre de chemin
func UserPage(c *gin.Context) {
filepath := c.Param("filepath")
c.String(http.StatusOK, "filepath is %s", filepath)
}Exemple 1
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'username is jack
userid is 001Exemple 2
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'filepath is /img/fruit.pngParamètres URL
Paramètres URL traditionnels, le format est /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)
}Exemple 1
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'username is jack
userid is 001Exemple 2
curl --location --request GET '127.0.0.1:8080/findUser'username is defaultUser
userid isParamètres de formulaire
Les types de contenu de formulaire sont généralement 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"])
}Exemple 1 : utiliser 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]La méthode PostForm analyse par défaut les formulaires de type application/x-www-form-urlencoded et multipart/form-data.
Exemple 2 : utiliser 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]Analyse des données
Dans la plupart des cas, nous utilisons des structures pour porter les données, au lieu d'analyser directement les paramètres. Dans gin, les méthodes principales pour la liaison de données sont Bind() et ShouldBind(), la différence est que la première appelle en interne ShouldBind(), et en cas de retour d'err, elle répond directement avec 400, tandis que la seconde ne le fait pas. Si vous voulez une gestion des erreurs plus flexible, il est recommandé de choisir la seconde. Ces deux fonctions déduiront automatiquement la méthode d'analyse en fonction du content-type de la requête.
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
// Appelle ShouldBindWith()
if err := c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // Répond directement 400 badrequest
return err
}
return nil
}Si vous voulez choisir vous-même, vous pouvez utiliser BindWith() et ShouldBindWith(), par exemple
c.MustBindWith(obj, binding.JSON) // json
c.MustBindWith(obj, binding.XML) // xmlLes types de liaison supportés par gin sont les suivants :
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{}
)Exemple
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
// Utiliser ShouldBind pour laisser gin déduire automatiquement
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)
}Liaison de données JSON
curl --location --request POST '127.0.0.1:8080/loginWithJSON' \
--header 'Content-Type: application/json' \
--data-raw '{
"username":"root",
"password":"root"
}'login successfully !Liaison de données de formulaire
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'login successfully !Liaison de données URL
curl --location --request GET '127.0.0.1:8080/loginWithQuery/root/root'login failed !À ce stade, une erreur se produit car le content-type de sortie est une chaîne vide, impossible de déduire comment analyser les données. Donc lors de l'utilisation de paramètres URL, nous devrions spécifier manuellement la méthode d'analyse, par exemple :
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 !")
}Liaisons multiples
Les méthodes générales lient les données en appelant c.Request.Body, mais vous ne pouvez pas appeler cette méthode plusieurs fois, par exemple c.ShouldBind, non réutilisable. Si vous voulez lier plusieurs fois, vous pouvez utiliser c.ShouldBindBodyWith.
func SomeHandler(c *gin.Context) {
objA := formA{}
objB := formB{}
// Lit c.Request.Body et stocke le résultat dans le contexte.
if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
c.String(http.StatusOK, `the body should be formA`)
// À ce moment, réutilise le body stocké dans le contexte.
}
if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
c.String(http.StatusOK, `the body should be formB JSON`)
// Peut accepter d'autres formats
}
if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
c.String(http.StatusOK, `the body should be formB XML`)
}
}TIP
c.ShouldBindBodyWith stocke le body dans le contexte avant la liaison. Cela a un léger impact sur les performances, si une seule liaison suffit, n'utilisez pas cette méthode. Seuls certains formats ont besoin de cette fonctionnalité, comme JSON, XML, MsgPack, ProtoBuf. Pour d'autres formats comme Query, Form, FormPost, FormMultipart, vous pouvez appeler c.ShouldBind() plusieurs fois sans perte de performance.
Validation des données
L'outil de validation intégré de gin est en fait github.com/go-playground/validator/v10, la méthode d'utilisation est presque identique, Validator
Exemple simple
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"
}'Sortie
invalid user,Key: 'LoginUser.Password' Error:Field validation for 'Password' failed on the 'required' tagTIP
Il faut noter que dans gin, le tag de validation de validator est binding, tandis que lors de l'utilisation autonome de validator, le tag de validation est validator
Réponse de données
La réponse de données est la dernière étape du traitement d'une interface. Le backend traite toutes les données, puis les retourne à l'appelant via le protocole HTTP. Gin fournit un support intégré riche pour la réponse de données, avec une utilisation concise et claire, très facile à prendre en main.
Exemple simple
func Hello(c *gin.Context) {
// Retourne des données en format chaîne pure, http.StatusOK représente le code de statut 200, les données sont "Hello world !"
c.String(http.StatusOK, "Hello world !")
}Rendu HTML
TIP
Lors du chargement de fichiers, le chemin racine par défaut est le chemin du projet, c'est-à-dire le chemin où se trouve le fichier go.mod. Dans l'exemple ci-dessous, index.html est situé à la racine du projet. Cependant, en général, ces fichiers de template ne sont pas placés à la racine, mais dans un dossier de ressources statiques.
func main() {
e := gin.Default()
// Charger les fichiers HTML, peut aussi utiliser Engine.LoadHTMLGlob()
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/'Retour
<!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>Réponse rapide
Précédemment, nous avons souvent utilisé la méthode context.String() pour répondre avec des données, c'est la méthode de réponse la plus basique, retournant directement une chaîne. En fait, gin a aussi de nombreuses méthodes de réponse rapide intégrées, par exemple :
// Utilise Render pour écrire les en-têtes de réponse et effectuer le rendu des données
func (c *Context) Render(code int, r render.Render)
// Effectue le rendu d'un template HTML, name est le chemin html, obj est le contenu
func (c *Context) HTML(code int, name string, obj any)
// Effectue le rendu en chaîne JSON indentée (beautifiée), généralement déconseillé car cela consomme plus de bande passante.
func (c *Context) IndentedJSON(code int, obj any)
// JSON sécurisé, peut prévenir le détournement JSON, plus de détails : https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)
// Rendu en mode JSONP
func (c *Context) JSONP(code int, obj any)
// Rendu en mode JSON
func (c *Context) JSON(code int, obj any)
// Rendu en mode JSON, convertit les codes unicode en ASCII
func (c *Context) AsciiJSON(code int, obj any)
// Rendu en mode JSON, sans échapper les caractères spéciaux HTML
func (c *Context) PureJSON(code int, obj any)
// Rendu en mode XML
func (c *Context) XML(code int, obj any)
// Rendu en mode YAML
func (c *Context) YAML(code int, obj any)
// Rendu en mode TOML
func (c *Context) TOML(code int, obj interface{})
// Rendu en mode ProtoBuf
func (c *Context) ProtoBuf(code int, obj any)
// Rendu en mode String
func (c *Context) String(code int, format string, values ...any)
// Redirige vers un emplacement spécifique
func (c *Context) Redirect(code int, location string)
// Écrit les données dans le flux de réponse
func (c *Context) Data(code int, contentType string, data []byte)
// Lit le flux via reader et l'écrit dans le flux de réponse
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)
// Écrit efficacement un fichier dans le flux de réponse
func (c *Context) File(filepath string)
// Écrit le flux de fichier depuis fs dans le flux de réponse de manière efficace
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)
// Écrit le flux de fichier depuis fs dans le flux de réponse de manière efficace, et le client le téléchargera avec le nom de fichier spécifié
func (c *Context) FileAttachment(filepath, filename string)
// Écrit le flux push du serveur dans le flux de réponse
func (c *Context) SSEvent(name string, message any)
// Envoie une réponse en flux et retourne un booléen pour déterminer si le client a interrompu le flux
func (c *Context) Stream(step func(w io.Writer) bool) boolPour la plupart des applications, context.JSON est le plus utilisé, les autres le sont relativement moins. Nous ne donnerons pas d'exemples ici car ils sont assez simples et faciles à comprendre, c'est essentiellement du direct.
Traitement asynchrone
Dans gin, le traitement asynchrone doit être combiné avec des goroutines, l'utilisation est très simple.
// copy retourne une copie du Context actuel pour une utilisation sécurisée en dehors de la portée du Context actuel, peut être passée à une goroutine
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() {
// La sous-routine devrait utiliser une copie du Context, pas le Context original
log.Println("Fonction de traitement asynchrone: ", ctx.HandlerNames())
}()
log.Println("Fonction de traitement d'interface: ", c.HandlerNames())
c.String(http.StatusOK, "hello")
}Test
curl --location --request GET 'http://localhost:8080/hello'Sortie
2022/12/21 13:33:47 Fonction de traitement asynchrone: []
2022/12/21 13:33:47 Fonction de traitement d'interface: [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"On peut voir que les deux sorties sont différentes, la copie supprime les valeurs de nombreux éléments pour des raisons de sécurité lors de la copie.
Transfert de fichiers
Le transfert de fichiers est une fonction indispensable des applications Web. Gin fournit un support très simple pour cela, mais en essence, le processus est le même qu'avec le net/http natif. Le processus consiste à lire le flux de fichier depuis le corps de la requête, puis à l'enregistrer localement.
Upload de fichier unique
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
log.Fatalln(e.Run(":8080"))
}
func uploadFile(ctx *gin.Context) {
// Récupérer le fichier
file, err := ctx.FormFile("file")
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// Enregistrer localement
err = ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "%+v", err)
return
}
// Retourner le résultat
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"'Résultat
upload a.jpg size:1424 byte successfully!TIP
En général, la Method pour l'upload de fichiers est spécifiée comme POST. Certaines entreprises peuvent préférer utiliser PUT, le premier est une requête HTTP simple, le second est une requête HTTP complexe, la différence spécifique n'est pas détaillée ici. Si vous utilisez le second, surtout dans des projets avec frontend et backend séparés, vous devez effectuer une configuration CORS appropriée, et la configuration par défaut de Gin ne supporte pas CORS Configuration CORS.
Upload de fichiers multiples
func main() {
e := gin.Default()
e.POST("/upload", uploadFile)
e.POST("/uploadFiles", uploadFiles)
log.Fatalln(e.Run(":8080"))
}
func uploadFiles(ctx *gin.Context) {
// Récupérer le formulaire multipart analysé par gin
form, _ := ctx.MultipartForm()
// Récupérer la liste de fichiers correspondant à la clé
files := form.File["files"]
// Parcourir la liste de fichiers, enregistrer localement
for _, file := range files {
err := ctx.SaveUploadedFile(file, "./"+file.Filename)
if err != nil {
ctx.String(http.StatusBadRequest, "upload failed")
return
}
}
// Retourner le résultat
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"'Sortie
upload 3 files successfully!Téléchargement de fichiers
Pour la partie téléchargement de fichiers, Gin encapsule à nouveau l'API de la bibliothèque standard, rendant le téléchargement de fichiers extrêmement simple.
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) {
// Récupérer le nom du fichier
filename := ctx.Param("filename")
// Retourner le fichier correspondant
ctx.FileAttachment(filename, filename)
}Test
curl --location --request GET 'http://localhost:8080/download/a.jpg'Résultat
Content-Disposition: attachment; filename="a.jpg"
Date: Wed, 21 Dec 2022 08:04:17 GMT
Last-Modified: Wed, 21 Dec 2022 07:50:44 GMTTrouvez-vous que c'est un peu trop simple ? Essayons d'écrire le processus nous-mêmes sans utiliser les méthodes du framework
func download(ctx *gin.Context) {
// Récupérer les paramètres
filename := ctx.Param("filename")
// Objet réponse et objet requête
response, request := ctx.Writer, ctx.Request
// Écrire les en-têtes de réponse
// response.Header().Set("Content-Type", "application/octet-stream") transfert de fichier en flux binaire
response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // Échappement sécurisé du nom de fichier
response.Header().Set("Content-Transfer-Encoding", "binary") // Encodage de transfert
http.ServeFile(response, request, filename)
}En fait, net/http est déjà suffisamment bien encapsulé.
TIP
Vous pouvez définir la mémoire maximale pour le transfert de fichiers via Engine.MaxMultipartMemory, par défaut 32 << 20 // 32 MB
Gestion des routes
La gestion des routes est une partie très importante d'un système, il faut s'assurer que chaque requête est correctement mappée à la fonction correspondante.
Groupes de routes
Créer un groupe de routes permet de classer les interfaces, différentes catégories d'interfaces correspondent à différentes fonctionnalités, et c'est aussi plus facile à gérer.
func Hello(c *gin.Context) {
}
func Login(c *gin.Context) {
}
func Update(c *gin.Context) {
}
func Delete(c *gin.Context) {
}Supposons que nous ayons les quatre interfaces ci-dessus, sans nous soucier de leur implémentation interne, Hello, Login sont un groupe, Update, Delete sont un autre groupe.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroupLors de la création d'un groupe, nous pouvons aussi enregistrer des gestionnaires pour la route racine du groupe, mais la plupart du temps ce n'est pas fait.
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)
}
}Nous les avons divisés en deux groupes v1 et v2. Les accolades {} ne servent qu'à la standardisation, indiquant que les gestionnaires enregistrés dans les accolades appartiennent au même groupe de routes, fonctionnellement elles n'ont aucun effet. De même, gin supporte aussi les groupes imbriqués, la méthode est la même que dans l'exemple ci-dessus, nous ne le démontrerons pas ici.
Route 404
Dans gin, la structure Engine fournit une méthode NoRoute, pour définir comment traiter une URL inexistante, les développeurs peuvent écrire la logique dans cette méthode pour qu'elle soit appelée automatiquement lorsque la route n'est pas trouvée, par défaut elle retourne le code de statut 404.
func (engine *Engine) NoRoute(handlers ...HandlerFunc)Prenons l'exemple précédent
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)
}
// Enregistrer le gestionnaire
e.NoRoute(func(context *gin.Context) { // Ceci est juste une démonstration, ne retournez pas de code HTML directement en production
context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
})
log.Fatalln(e.Run(":8080"))
}Envoyez une requête arbitraire
curl --location --request GET 'http://localhost:8080/'<h1>404 Page Not Found</h1>Route 405
Dans les codes de statut HTTP, 405 signifie que la méthode de requête actuelle n'est pas autorisée. Gin fournit la méthode suivante
func (engine *Engine) NoMethod(handlers ...HandlerFunc)pour enregistrer un gestionnaire, à appeler automatiquement en cas d'erreur, à condition de définir Engine.HandleMethodNotAllowed = true.
func main() {
e := gin.Default()
// Il faut le définir à true
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>")
})
// Enregistrer le gestionnaire
e.NoMethod(func(context *gin.Context) {
context.String(http.StatusMethodNotAllowed, "method not allowed")
})
log.Fatalln(e.Run(":8080"))
}Après configuration, le header par défaut de gin ne supporte pas les requêtes OPTION, testons
curl --location --request OPTIONS 'http://localhost:8080/v2/delete'method not allowedConfiguration réussie.
Redirection
La redirection dans gin est très simple, il suffit d'appeler la méthode gin.Context.Redirect().
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/'Sortie
helloMiddleware
Gin est très léger et flexible, très extensible, et le support des middlewares est très convivial. Dans Gin, toutes les requêtes d'interface passent par des middlewares, via lesquels les développeurs peuvent implémenter de nombreuses fonctionnalités et logiques personnalisées. Bien que Gin lui-même ait peu de fonctionnalités intégrées, il existe de nombreux middlewares d'extension gin développés par la communauté tierce.
Essentiellement, un middleware est aussi un gestionnaire d'interface
// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)Dans un certain sens, chaque gestionnaire correspondant à une requête est aussi un middleware, juste avec une portée très locale.
func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}En regardant le code source de gin, la fonction Default retourne un Engine par défaut qui utilise deux middlewares par défaut Logger() et Recovery(). Si vous ne voulez pas utiliser les middlewares par défaut, vous pouvez utiliser gin.New() à la place.
Middleware global
Un middleware global a une portée globale, toutes les requêtes du système passeront par ce middleware.
func GlobalMiddleware() gin.HandlerFunc {
return func(ctx *gin.Context) {
fmt.Println("Middleware global exécuté...")
}
}D'abord, créez une fonction de fermeture pour créer le middleware, puis enregistrez le middleware global via Engine.Use().
func main() {
e := gin.Default()
// Enregistrer le middleware global
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'Sortie
[GIN-debug] Listening and serving HTTP on :8080
Middleware global exécuté...
[GIN] 2022/12/21 - 11:57:52 | 200 | 538.9µs | ::1 | GET "/v1/hello"Middleware local
Un middleware local a une portée locale, seules certaines requêtes du système passeront par ce middleware. Un middleware local peut être enregistré sur une seule route, mais plus souvent sur un groupe de routes.
func main() {
e := gin.Default()
// Enregistrer le middleware global
e.Use(GlobalMiddleware())
// Enregistrer le middleware local du groupe de routes
v1 := e.Group("/v1", LocalMiddleware())
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
// Enregistrer le middleware local d'une seule route
v2.POST("/update", LocalMiddleware(), Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}Test
curl --location --request POST 'http://localhost:8080/v2/update'Sortie
Middleware global exécuté...
Middleware local exécuté
[GIN] 2022/12/21 - 12:05:03 | 200 | 999.9µs | ::1 | POST "/v2/update"Principe du middleware
L'utilisation et la personnalisation des middlewares Gin sont très faciles, le principe interne est aussi assez simple. Pour les apprentissages ultérieurs, il faut comprendre brièvement le principe interne. Les middlewares Gin utilisent le pattern de chaîne de responsabilité. Context maintient une HandlersChain, essentiellement un []HandlerFunc, et un index, de type int8. Dans la méthode Engine.handlerHTTPRequest(c *Context), un segment de code montre le processus d'appel : après que gin a trouvé la route correspondante dans l'arbre de routage, il appelle la méthode Next().
if value.handlers != nil {
// Assigne la chaîne d'appel au Context
c.handlers = value.handlers
c.fullPath = value.fullPath
// Appelle le middleware
c.Next()
c.writermem.WriteHeaderNow()
return
}L'appel de Next() est la clé. Next() parcourt les HandlerFunc dans les handlers de la route et les exécute. À ce moment, on peut voir que le rôle de index est d'enregistrer la position d'appel du middleware. Parmi eux, la fonction d'interface enregistrée pour la route correspondante est aussi dans handlers, c'est pourquoi on a dit plus tôt qu'une interface est aussi un middleware.
func (c *Context) Next() {
// +1 dès l'entrée pour éviter une récursion infinie, la valeur par défaut est -1
c.index++
for c.index < int8(len(c.handlers)) {
// Exécuter HandlerFunc
c.handlers[c.index](c)
// Exécution terminée, index+1
c.index++
}
}Modifions la logique de Hello() pour vérifier si c'est bien le cas
func Hello(c *gin.Context) {
fmt.Println(c.HandlerNames())
}Résultat de sortie
[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.GlobalMiddleware.func1 main.LocalMiddleware.func1 main.Hello]On peut voir que l'ordre de la chaîne d'appel des middlewares est : Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello, le dernier élément de la chaîne d'appel est la vraie fonction d'interface à exécuter, les précédents sont tous des middlewares.
TIP
Lors de l'enregistrement d'une route locale, il y a l'assertion suivante
finalSize := len(group.Handlers) + len(handlers) // Nombre total de middlewares
assert1(finalSize < int(abortIndex), "too many handlers")Où abortIndex int8 = math.MaxInt8 >> 1 vaut 63, c'est-à-dire que le nombre de routes enregistrées dans le système ne doit pas dépasser 63.
Middleware minuteur
Après avoir compris le principe des middlewares ci-dessus, nous pouvons écrire un simple middleware de statistiques de temps de requête.
func TimeMiddleware() gin.HandlerFunc {
return func(context *gin.Context) {
// Enregistrer l'heure de début
start := time.Now()
// Exécuter la chaîne d'appel suivante
context.Next()
// Calculer l'intervalle de temps
duration := time.Since(start)
// Afficher en nanosecondes pour observer le résultat
fmt.Println("Temps de requête: ", duration.Nanoseconds())
}
}
func main() {
e := gin.Default()
// Enregistrer le middleware global, middleware minuteur
e.Use(GlobalMiddleware(), TimeMiddleware())
// Enregistrer le middleware local du groupe de routes
v1 := e.Group("/v1", LocalMiddleware())
{
v1.GET("/hello", Hello)
v1.GET("/login", Login)
}
v2 := e.Group("/v2")
{
// Enregistrer le middleware local d'une seule route
v2.POST("/update", LocalMiddleware(), Update)
v2.DELETE("/delete", Delete)
}
log.Fatalln(e.Run(":8080"))
}Test
curl --location --request GET 'http://localhost:8080/v1/hello'Sortie
Temps de requête: 517600Un simple middleware minuteur a été écrit, vous pourrez ensuite écrire des middlewares plus pratiques en explorant par vous-même.
Configuration du service
Utiliser uniquement la configuration par défaut est loin d'être suffisant. Dans la plupart des cas, de nombreuses configurations de service doivent être modifiées pour répondre aux besoins.
Configuration HTTP
Vous pouvez créer un Server via net/http pour configurer, Gin lui-même supporte aussi l'utilisation comme l'API native.
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())
}Configuration des ressources statiques
Les ressources statiques étaient auparavant une partie indispensable du côté serveur, bien que leur utilisation diminue progressivement, il y a encore de nombreux systèmes utilisant une architecture monolithique.
Gin fournit trois méthodes pour charger des ressources statiques
// Charger un dossier de fichiers statiques
func (group *RouterGroup) Static(relativePath, root string) IRoutes
// Charger un fs
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes
// Charger un fichier statique
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutesTIP
relativePath est le chemin relatif mappé sur l'URL web, root est le chemin réel du fichier dans le projet
Supposons que la structure du projet est la suivante
root
|
|-- static
| |
| |-- a.jpg
| |
| |-- favicon.ico
|
|-- view
|
|-- htmlfunc main() {
router := gin.Default()
// Charger le répertoire de fichiers statiques
router.Static("/static", "./static")
// Charger le répertoire de fichiers statiques
router.StaticFS("/view", http.Dir("view"))
// Charger un fichier statique
router.StaticFile("/favicon", "./static/favicon.ico")
router.Run(":8080")
}Configuration CORS
Gin lui-même ne fait aucun traitement pour la configuration CORS, vous devez écrire votre propre middleware pour implémenter les besoins correspondants. En fait, ce n'est pas très difficile, toute personne connaissant un peu le protocole HTTP peut l'écrire, la logique est fondamentalement la même.
func CorsMiddle() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
origin := c.Request.Header.Get("Origin")
if origin != "" {
// En production, le serveur ne remplit généralement pas *, il devrait remplir le domaine spécifié
c.Header("Access-Control-Allow-Origin", origin)
// Méthodes HTTP autorisées
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
// En-têtes de requête autorisés
c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
// En-têtes de réponse accessibles par le client
c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
// Indique si les informations d'identification doivent être transportées Credentials peut être des cookies, des en-têtes d'autorisation ou des certificats client TLS
// Quand défini à true, Access-Control-Allow-Origin ne peut pas être *
c.Header("Access-Control-Allow-Credentials", "true")
}
// Laisser passer les requêtes OPTION, mais ne pas exécuter les méthodes suivantes
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
// Laisser passer
c.Next()
}
}Enregistrez le middleware comme middleware global.
Contrôle de session
À l'époque actuelle, il existe trois méthodes populaires de contrôle de session Web : cookie, session, JWT.
Cookie
Les informations dans les cookies sont stockées sous forme de paires clé-valeur dans le navigateur, et les données sont directement visibles dans le navigateur.
Avantages :
- Structure simple
- Données persistantes
Inconvénients :
Taille limitée
Stockage en clair
Vulnérable aux attaques CSRF
import (
"fmt"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.Default()
router.GET("/cookie", func(c *gin.Context) {
// Récupérer le cookie correspondant
cookie, err := c.Cookie("gin_cookie")
if err != nil {
cookie = "NotSet"
// Définir le cookie Paramètres : clé, val, durée de vie, répertoire, domaine, si l'accès via js est autorisé, http uniquement
c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
}
fmt.Printf("Cookie value: %s \n", cookie)
})
router.Run()
}Les cookies simples étaient beaucoup utilisés il y a cinq ou six ans, mais l'auteur utilise rarement les cookies seuls pour le contrôle de session, c'est vraiment pas très sécurisé.
Session
La session est stockée sur le serveur, puis un cookie est envoyé pour être stocké dans le navigateur. Le cookie stocke le session_id, ensuite à chaque requête le serveur peut récupérer les informations de session correspondantes via le session_id.
Avantages :
- Stockée sur le serveur, sécurité accrue, gestion facilitée
Inconvénients :
- Stockée sur le serveur, augmente la charge du serveur, réduit les performances
- Basée sur les cookies pour l'identification, pas sécurisé
- Les informations d'authentification ne sont pas synchronisées en environnement distribué
Session et Cookie sont inséparables, chaque fois que vous utilisez une Session, par défaut vous utilisez aussi un Cookie. Gin ne supporte pas les Sessions par défaut, car Cookie fait partie du protocole HTTP, mais pas Session. Cependant, il existe un middleware tiers qui le supporte, installez simplement la dépendance. Adresse du dépôt : gin-contrib/sessions: Gin middleware for session management (github.com)
go get github.com/gin-contrib/sessionsSupporte cookie, Redis, MongoDB, GORM, PostgreSQL
func main() {
r := gin.Default()
// Créer un moteur de stockage basé sur Cookie
store := cookie.NewStore([]byte("secret"))
// Définir le middleware Session, mysession est le nom de session, aussi le nom du cookie
r.Use(sessions.Sessions("mysession", store))
r.GET("/incr", func(c *gin.Context) {
// Initialiser la session
session := sessions.Default(c)
var count int
// Récupérer la valeur
v := session.Get("count")
if v == nil {
count = 0
} else {
count = v.(int)
count++
}
// Définir
session.Set("count", count)
// Sauvegarder
session.Save()
c.JSON(200, gin.H{"count": count})
})
r.Run(":8000")
}Il n'est généralement pas recommandé de stocker la Session via Cookie, l'utilisation de Redis est recommandée, pour d'autres exemples veuillez consulter le dépôt officiel.
JWT
Avantages :
- Basé sur JSON, commun à plusieurs langages
- Peut stocker des informations non sensibles
- Très petite empreinte, facile à transporter
- Pas de stockage côté serveur, favorable à l'extension distribuée
Inconvénients :
- Problème de rafraîchissement du Token
- Une fois émis, il ne peut pas être contrôlé activement
Depuis la révolution frontend, les programmeurs frontend ne sont plus seulement des "rédacteurs de pages", la tendance à la séparation frontend/backend s'intensifie, et JWT est le meilleur choix pour le contrôle de session dans les systèmes séparés et distribués, avec de grands avantages naturels. Considérant que JWT est complètement détaché du contenu de Gin, et qu'aucun middleware ne le supporte, car JWT lui-même n'est limité à aucun framework ni langage, nous ne le détaillerons pas ici. Vous pouvez consulter un autre document : JWT
Gestion des logs
Le middleware de log utilisé par défaut par Gin est os.Stdout, avec seulement les fonctionnalités les plus basiques. Après tout, Gin se concentre uniquement sur le service Web, dans la plupart des cas vous devriez utiliser un framework de log plus mature, mais ce n'est pas dans le cadre de ce chapitre, et l'extensibilité de Gin est très élevée, il peut être facilement intégré avec d'autres frameworks. Ici, nous ne discutons que de son service de log intégré.
Couleur de la console
gin.DisableConsoleColor() // Désactiver la couleur des logs de consoleSauf en développement, la plupart du temps il n'est pas recommandé d'activer cette option.
Écrire les logs dans un fichier
func main() {
e := gin.Default()
// Désactiver la couleur de la console
gin.DisableConsoleColor()
// Créer deux fichiers de log
log1, _ := os.Create("info1.log")
log2, _ := os.Create("info2.log")
// Enregistrer simultanément dans deux fichiers de log
gin.DefaultWriter = io.MultiWriter(log1, log2)
e.GET("/hello", Hello)
log.Fatalln(e.Run(":8080"))
}Le log intégré de gin supporte l'écriture dans plusieurs fichiers, mais le contenu est identique, ce qui n'est pas très pratique à utiliser, et les logs de requêtes ne sont pas écrits dans les fichiers.
func main() {
router := gin.New()
// Le middleware LoggerWithFormatter écrira les logs dans gin.DefaultWriter
// Par défaut gin.DefaultWriter = os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
//TODO logique d'écriture dans le fichier correspondant
......
// Sortie au format personnalisé
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")
}En personnalisant le middleware, vous pouvez implémenter l'écriture des logs dans des fichiers.
Format des logs de débogage de routage
Ici, nous ne modifions que les logs affichant les informations de routage au démarrage.
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"))
}Sortie
2022/12/21 17:19:13 Route GET /hello main.Hello 3Conclusion : Gin est l'un des frameworks Web les plus faciles à apprendre en Go, car Gin a vraiment minimalisé ses responsabilités, se concentrant uniquement sur le service Web. Les autres logiques d'authentification, de cache de données, etc. sont laissées aux développeurs. Comparé aux frameworks complets et complexes, le Gin léger et simple est plus adapté et devrait être appris par les débutants, car Gin n'impose pas une norme particulière, la façon de construire le projet et la structure à adopter sont laissées à votre discrétion, ce qui permet aux débutants de développer leurs compétences.
