Skip to content

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.

powershell
go get -u github.com/gin-gonic/gin

Import

go
import "github.com/gin-gonic/gin"

Démarrage rapide

go
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

http
GET localhost:8080/ping

Retour

http
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.json

Documentation

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.

go
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

bash
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'
username is jack
 userid is 001

Exemple 2

bash
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'
filepath is  /img/fruit.png

Paramètres URL

Paramètres URL traditionnels, le format est /url?key=val&key1=val1&key2=val2.

go
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

bash
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'
username is jack
userid is 001

Exemple 2

bash
curl --location --request GET '127.0.0.1:8080/findUser'
username is defaultUser
userid is

Paramè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.

go
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

bash
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

bash
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.

go
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

go
c.MustBindWith(obj, binding.JSON) // json
c.MustBindWith(obj, binding.XML) // xml

Les types de liaison supportés par gin sont les suivants :

go
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

go
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

bash
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

go
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'
login successfully !

Liaison de données URL

go
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 :

go
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.

go
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

go
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' tag

TIP

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

go
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.

go
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

html
<!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 :

go
// 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) bool

Pour 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.

go
// 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() *Context
go
func 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

go
curl --location --request GET 'http://localhost:8080/hello'

Sortie

go
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

go
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

go
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.

go
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 GMT

Trouvez-vous que c'est un peu trop simple ? Essayons d'écrire le processus nous-mêmes sans utiliser les méthodes du framework

go
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.

go
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.

go
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup

Lors 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.

go
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.

go
func (engine *Engine) NoRoute(handlers ...HandlerFunc)

Prenons l'exemple précédent

go
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

go
func (engine *Engine) NoMethod(handlers ...HandlerFunc)

pour enregistrer un gestionnaire, à appeler automatiquement en cas d'erreur, à condition de définir Engine.HandleMethodNotAllowed = true.

go
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 allowed

Configuration réussie.

Redirection

La redirection dans gin est très simple, il suffit d'appeler la méthode gin.Context.Redirect().

go
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

hello

Middleware

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

go
// 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.

go
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.

go
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().

go
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.

go
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().

go
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.

go
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

go
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

go
finalSize := len(group.Handlers) + len(handlers) // Nombre total de middlewares
assert1(finalSize < int(abortIndex), "too many handlers")

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.

go
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:  517600

Un 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.

go
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

go
// 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) IRoutes

TIP

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
  |
  |-- html
go
func 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.

go
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.

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

go
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/sessions

Supporte cookie, Redis, MongoDB, GORM, PostgreSQL

go
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

go
gin.DisableConsoleColor() // Désactiver la couleur des logs de console

Sauf en développement, la plupart du temps il n'est pas recommandé d'activer cette option.

Écrire les logs dans un fichier

go
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.

go
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.

go
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 3

Conclusion : 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.

Golang by www.golangdev.cn edit