Skip to content

Gin

Documentazione ufficiale: Gin Web Framework (gin-gonic.com)

Repository: gin-gonic/gin: Gin is a HTTP web framework written in Go (Golang)

Esempi ufficiali: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

Introduzione

Gin è un framework web scritto in Go (Golang). Ha un'API simile a martini, con prestazioni molto migliori, grazie a httprouter, la velocità è aumentata di 40 volte. Se hai bisogno di prestazioni e buona produttività, ti piacerà sicuramente Gin. Gin rispetto a Iris e Beego tende ad essere un framework più leggero, responsabile solo della parte Web, persegue prestazioni estreme del routing, le funzioni potrebbero non essere così complete, ma vince per leggerezza ed estensibilità, questo è anche il suo punto di forza. Pertanto, tra tutti i framework Web, Gin è il più facile da imparare e usare.

Caratteristiche

  • Veloce: Routing basato su albero Radix, occupazione di memoria ridotta. Nessuna riflessione. Prestazioni API prevedibili.
  • Supporto middleware: Le richieste HTTP in arrivo possono essere gestite da una serie di middleware e operazioni finali. Ad esempio: Logger, Authorization, GZIP, operazione finale DB.
  • Gestione Crash: Gin può catturare un panic che si verifica in una richiesta HTTP e recuperarlo. In questo modo, il tuo server sarà sempre disponibile.
  • Validazione JSON: Gin può analizzare e validare il JSON della richiesta, ad esempio verificare la presenza di valori richiesti.
  • Gruppi di routing: Organizza meglio le route. È necessaria l'autorizzazione, diverse versioni API... Inoltre, questi gruppi possono essere annidati senza limitazioni senza ridurre le prestazioni.
  • Gestione errori: Gin fornisce un modo conveniente per raccogliere tutti gli errori che si verificano durante le richieste HTTP. Infine, i middleware possono scriverli nei file di log, nel database e inviarli attraverso la rete.
  • Rendering integrato: Gin fornisce API facili da usare per il rendering JSON, XML e HTML.
  • Estensibilità: Creare un nuovo middleware è molto semplice

Installazione

Fino ad oggi 2022/11/22, la versione minima di Go supportata da gin è 1.16, si consiglia di utilizzare go mod per gestire le dipendenze del progetto.

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

Import

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

Inizio Rapido

go
package main

import (
   "github.com/gin-gonic/gin"
   "net/http"
)

func main() {
   engine := gin.Default() // Crea motore gin
   engine.GET("/ping", func(context *gin.Context) {
      context.JSON(http.StatusOK, gin.H{
         "message": "pong",
      })
   })
   engine.Run() // Avvia server, ascolta default su localhost:8080
}

Richiesta URL

http
GET localhost:8080/ping

Risposta

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"
}

Documentazione

In realtà, la documentazione ufficiale di Gin non contiene molti tutorial, la maggior parte sono solo introduzioni, utilizzi di base ed esempi, ma sotto gin-gonic/, c'è un repository gin-gonic/examples, un repository di esempi di gin mantenuto dalla comunità. Sono tutti in inglese, non aggiornati molto frequentemente, l'autore ha anche iniziato a imparare il framework gin da qui lentamente.

Repository esempi: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

TIP

Prima di iniziare si consiglia di leggere HttpRouter: HttpRouter

Analisi Parametri

L'analisi dei parametri in gin supporta tre metodi in totale: parametri route, parametri URL, parametri form, di seguito vengono spiegati uno per uno con esempi di codice, semplici e facili da capire.

Parametri Route

I parametri route sono in realtà un'incapsulamento dell'analisi dei parametri di HttpRouter, l'utilizzo è fondamentalmente coerente con 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"))
}

// Esempio parametro denominato
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)
}

// Esempio parametro percorso
func UserPage(c *gin.Context) {
   filepath := c.Param("filepath")
   c.String(http.StatusOK, "filepath is  %s", filepath)
}

Esempio uno

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

Esempio due

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

Parametri URL

I parametri URL tradizionali, il formato è /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)
}

Esempio uno

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

Esempio due

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

Parametri Form

Il tipo di contenuto del form è generalmente 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"])
}

Esempio uno: Usa 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]

Il metodo PostForm analizza per default i form di tipo application/x-www-form-urlencoded e multipart/form-data.

Esempio due: Usa 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]

Analisi Dati

Nella maggior parte dei casi useremo una struct per trasportare dati, invece di analizzare direttamente i parametri. In gin, i metodi utilizzati per il binding dei dati sono principalmente Bind() e ShouldBind(), la differenza tra i due è che il primo internally chiama direttamente ShouldBind(), ovviamente quando restituisce err, eseguirà direttamente una risposta 400, il secondo no. Se si desidera una gestione degli errori più flessibile, si consiglia di scegliere quest'ultimo. Queste due funzioni dedurranno automaticamente in base al content-type della richiesta quale metodo di analisi utilizzare.

go
func (c *Context) MustBindWith(obj any, b binding.Binding) error {
    // Chiama ShouldBindWith()
  if err := c.ShouldBindWith(obj, b); err != nil {
    c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) // Risponde direttamente 400 badrequest
    return err
  }
  return nil
}

Se si desidera scegliere manualmente è possibile utilizzare BindWith() e ShouldBindWith(), ad esempio

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

I tipi di binding supportati da gin sono i seguenti:

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{}
)

Esempio

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
    // Usa ShouldBind per lasciare che gin deduca automaticamente
  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)
}

Binding Dati 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 !

Binding Dati Form

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

Binding Dati URL

go
curl --location --request GET '127.0.0.1:8080/loginWithQuery/root/root'
login failed !

Qui si verifica un errore, perché il content-type restituito qui è una stringa vuota, non è possibile dedurre come analizzare i dati. Quindi quando si utilizzano parametri URL, dovremmo specificare manualmente il metodo di analisi, ad esempio:

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 !")
}

Binding Multipli

Generalmente i metodi chiamano il metodo c.Request.Body per associare i dati, ma non è possibile chiamare questo metodo più volte, ad esempio c.ShouldBind, non riutilizzabile, se si desidera associare più volte è possibile utilizzare

c.ShouldBindBodyWith.

go
func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // Legge c.Request.Body e memorizza il risultato nel contesto.
  if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // A questo punto, riutilizza il body memorizzato nel contesto.
  }
  if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `the body should be formB JSON`)
  // Può accettare altri formati
  }
  if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
    c.String(http.StatusOK, `the body should be formB XML`)
  }
}

TIP

c.ShouldBindBodyWith memorizzerà il body nel contesto prima del binding. Questo avrà un leggero impatto sulle prestazioni, se è possibile completare il binding con una chiamata, allora non usare questo metodo. Solo alcuni formati necessitano di questa funzionalità, come JSON, XML, MsgPack, ProtoBuf. Per altri formati, come Query, Form, FormPost, FormMultipart è possibile chiamare c.ShouldBind() più volte senza alcuna perdita di prestazioni.

Validazione Dati

Lo strumento di validazione integrato in gin è in realtà github.com/go-playground/validator/v10, il metodo di utilizzo è quasi lo stesso, Validator

Esempio Semplice

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"

}'

Output

invalid user,Key: 'LoginUser.Password' Error:Field validation for 'Password' failed on the 'required' tag

TIP

Una cosa da notare è che il tag di validazione di validator in gin è binding, mentre il tag di validazione quando si usa validator da solo è validator

Risposta Dati

La risposta dati è l'ultimo passo da fare nell'elaborazione dell'interfaccia, il backend restituisce tutti i dati elaborati al chiamante tramite il protocollo HTTP, gin fornisce un ricco supporto integrato per la risposta dati, l'uso è semplice e chiaro, molto facile da imparare.

Esempio Semplice

go
func Hello(c *gin.Context) {
    // Restituisce dati in formato stringa pura, http.StatusOK rappresenta il codice di stato 200, i dati sono "Hello world !"
  c.String(http.StatusOK, "Hello world !")
}

Rendering HTML

TIP

Quando si caricano i file, il percorso radice predefinito è il percorso del progetto, ovvero il percorso in cui si trova il file go.mod, index.html negli esempi seguenti si trova in index.html nel percorso radice, ma in generale questi file di modello non vengono messi nel percorso radice, ma vengono archiviati nelle cartelle delle risorse statiche

go
func main() {
   e := gin.Default()
    // Carica file HTML, è possibile usare anche 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/'

Restituisce

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>

Risposta Rapida

In precedenza è stato spesso utilizzato il metodo context.String() per la risposta dati, questo è il metodo di risposta più originale, restituisce direttamente una stringa, in gin sono incorporati anche molti metodi di risposta rapida ad esempio:

go
// Usa Render per scrivere l'header di risposta ed eseguire il rendering dei dati
func (c *Context) Render(code int, r render.Render)

// Renderizza un template HTML, name è il percorso html, obj è il contenuto
func (c *Context) HTML(code int, name string, obj any)

// Renderizza dati con stringa JSON formattata con indentazione, generalmente non consigliato usare questo metodo, perché causerà maggiori costi di trasmissione.
func (c *Context) IndentedJSON(code int, obj any)

// JSON sicuro, può prevenire il JSON hijacking, dettagli: https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)

// Renderizza in modalità JSONP
func (c *Context) JSONP(code int, obj any)

// Renderizza in modalità JSON
func (c *Context) JSON(code int, obj any)

// Renderizza in modalità JSON, convertirà i codici unicode in ASCII
func (c *Context) AsciiJSON(code int, obj any)

// Renderizza in modalità JSON, non eseguirà l'escape dei caratteri speciali HTML
func (c *Context) PureJSON(code int, obj any)

// Renderizza in modalità XML
func (c *Context) XML(code int, obj any)

// Renderizza in modalità YML
func (c *Context) YAML(code int, obj any)

// Renderizza in modalità TOML
func (c *Context) TOML(code int, obj interface{})

// Renderizza in modalità ProtoBuf
func (c *Context) ProtoBuf(code int, obj any)

// Renderizza in modalità String
func (c *Context) String(code int, format string, values ...any)

// Reindirizza a una posizione specifica
func (c *Context) Redirect(code int, location string)

// Scrive data nel flusso di risposta
func (c *Context) Data(code int, contentType string, data []byte)

// Legge il flusso tramite reader e lo scrive nel flusso di risposta
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)

// Scrive in modo efficiente il file nel flusso di risposta
func (c *Context) File(filepath string)

// Scrive in modo efficiente il flusso di file da fs nel flusso di risposta
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)

// Scrive in modo efficiente il flusso di file da fs nel flusso di risposta, e il client scaricherà con il nome file specificato
func (c *Context) FileAttachment(filepath, filename string)

// Scrive il flusso di push del server nel flusso di risposta
func (c *Context) SSEvent(name string, message any)

// Invia una risposta di flusso e restituisce un valore booleano, per determinare se il client si è disconnesso a metà del flusso
func (c *Context) Stream(step func(w io.Writer) bool) bool

Per la maggior parte delle applicazioni, quella più utilizzata è context.JSON, le altre sono relativamente meno utilizzate, qui non farò esempi perché sono tutte semplici e facili da capire, quasi tutte chiamate dirette.

Elaborazione Asincrona

In gin, l'elaborazione asincrona deve essere combinata con goroutine, è molto semplice da usare.

go
// copy restituisce una copia del Context corrente per un uso sicuro al di fuori dell'ambito del Context corrente, può essere usata per passare a una 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 goroutine dovrebbe usare la copia del Context, non il Context originale
    log.Println("Funzione elaborazione asincrona: ", ctx.HandlerNames())
  }()
  log.Println("Funzione elaborazione interfaccia: ", c.HandlerNames())
  c.String(http.StatusOK, "hello")
}

Test

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

Output

go
2022/12/21 13:33:47 Funzione elaborazione asincrona:  []
2022/12/21 13:33:47 Funzione elaborazione interfaccia:  [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"

Si può vedere che i due output sono diversi, la copia durante la copia, per motivi di sicurezza, ha eliminato molti valori degli elementi.

Trasferimento File

Il trasferimento file è una funzione indispensabile per le applicazioni Web, il supporto di gin per questo è anche incapsulato in modo molto semplice, ma in sostanza è quasi lo stesso processo dell'uso nativo di net/http. Il processo è leggere il flusso di file dal corpo della richiesta, quindi salvarlo localmente.

Caricamento File Singolo

go
func main() {
  e := gin.Default()
  e.POST("/upload", uploadFile)
  log.Fatalln(e.Run(":8080"))
}

func uploadFile(ctx *gin.Context) {
  // Ottieni file
  file, err := ctx.FormFile("file")
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // Salva localmente
  err = ctx.SaveUploadedFile(file, "./"+file.Filename)
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // Restituisce risultato
  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"'

Risultato

upload a.jpg size:1424 byte successfully!

TIP

In generale, il Method per caricare file specificherà l'uso di POST, alcune aziende potrebbero preferire usare PUT, il primo è una richiesta HTTP semplice, il secondo è una richiesta HTTP complessa, le differenze specifiche non saranno spiegate qui, se si usa quest'ultimo, specialmente in progetti con separazione frontend-backend, è necessario eseguire la gestione CORS corrispondente, e la configurazione predefinita di Gin non supporta CORS Configurazione CORS.

Caricamento File Multipli

go
func main() {
   e := gin.Default()
   e.POST("/upload", uploadFile)
   e.POST("/uploadFiles", uploadFiles)
   log.Fatalln(e.Run(":8080"))
}

func uploadFiles(ctx *gin.Context) {
  // Ottieni form multipart analizzato da gin
  form, _ := ctx.MultipartForm()
  // Ottieni lista file corrispondente dalla chiave
  files := form.File["files"]
  // Itera lista file, salva localmente
  for _, file := range files {
    err := ctx.SaveUploadedFile(file, "./"+file.Filename)
    if err != nil {
      ctx.String(http.StatusBadRequest, "upload failed")
      return
    }
  }
  // Restituisce risultato
  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"'

Output

upload 3 files successfully!

Download File

Per la parte di download file, Gin ha incapsulato ulteriormente le API della libreria standard originale, rendendo il download file estremamente semplice.

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) {
    // Ottieni nome file
  filename := ctx.Param("filename")
    // Restituisce file corrispondente
  ctx.FileAttachment(filename, filename)
}

Test

curl --location --request GET 'http://localhost:8080/download/a.jpg'

Risultato

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

Non pensi che sia troppo semplice, potresti non usare il metodo del framework, scrivere il processo da solo

go
func download(ctx *gin.Context) {
   // Ottieni parametro
   filename := ctx.Param("filename")

   // Oggetto risposta e oggetto richiesta
   response, request := ctx.Writer, ctx.Request
   // Scrive header di risposta
   // response.Header().Set("Content-Type", "application/octet-stream")  Trasmette file come flusso binario
   response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // Esegue escape di sicurezza del nome file
   response.Header().Set("Content-Transfer-Encoding", "binary")                                            // Codifica trasmissione
   http.ServeFile(response, request, filename)
}

In realtà net/http è già stato incapsulato abbastanza bene

TIP

È possibile impostare la memoria massima per il trasferimento file tramite Engine.MaxMultipartMemory, il valore predefinito è 32 << 20 // 32 MB

Gestione Route

La gestione delle route è una parte molto importante di un sistema, è necessario garantire che ogni richiesta possa essere mappata correttamente alla funzione corrispondente.

Gruppi Route

Creare un gruppo di route significa classificare le interfacce, interfacce di categorie diverse corrispondono a funzioni diverse, ed è anche più facile da gestire.

go
func Hello(c *gin.Context) {

}

func Login(c *gin.Context) {

}

func Update(c *gin.Context) {

}

func Delete(c *gin.Context) {

}

Supponiamo di avere le quattro interfacce sopra, per ora non preoccupiamoci della loro implementazione interna, Hello, Login sono un gruppo, Update, Delete sono un gruppo.

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

Quando si crea un gruppo, è anche possibile registrare un handler per la route radice del gruppo, ma nella maggior parte dei casi non lo si fa.

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)
  }
}

Li abbiamo divisi in due gruppi v1, v2, le parentesi graffe {} servono solo come convenzione, indicano che gli handler registrati tra le parentesi graffe appartengono allo stesso gruppo di route, non hanno alcun effetto funzionale. Allo stesso modo, gin supporta anche gruppi annidati, il metodo è coerente con l'esempio sopra, qui non verrà dimostrato.

Route 404

La struct Engine in gin fornisce un metodo NoRoute, per impostare come gestire quando l'URL di accesso non esiste, gli sviluppatori possono scrivere la logica in questo metodo, in modo che venga chiamata automaticamente quando la route non viene trovata, per default restituirà il codice di stato 404

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

Prendiamo l'esempio precedente

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)
   }
   // Registra handler
   e.NoRoute(func(context *gin.Context) { // Questa è solo una dimostrazione, non restituire direttamente codice HTML in produzione
      context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
   })
   log.Fatalln(e.Run(":8080"))
}

Invia una richiesta qualsiasi

curl --location --request GET 'http://localhost:8080/'
<h1>404 Page Not Found</h1>

Route 405

Nel codice di stato Http, 405 indica che il tipo di metodo della richiesta corrente non è consentito, gin fornisce il seguente metodo

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

per registrare un handler, in modo che venga chiamato automaticamente quando si verifica, a condizione che sia impostato Engine.HandleMethodNotAllowed = true.

go
func main() {
   e := gin.Default()
   // Deve essere impostato su 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>")
   })
   // Registra handler
   e.NoMethod(func(context *gin.Context) {
      context.String(http.StatusMethodNotAllowed, "method not allowed")
   })
   log.Fatalln(e.Run(":8080"))
}

Dopo la configurazione, l'header predefinito di gin non supporta le richieste OPTION, testa

curl --location --request OPTIONS 'http://localhost:8080/v2/delete'
method not allowed

Configurazione completata

Reindirizzamento

Il reindirizzamento in gin è molto semplice, basta chiamare il metodo 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/'

Output

hello

Middleware

Gin è molto leggero e flessibile, ha un'altissima estensibilità, e il supporto per i middleware è anche molto amichevole. In Gin, tutte le richieste di interfaccia passano attraverso i middleware, attraverso i middleware gli sviluppatori possono implementare molte funzioni e logiche personalizzate, sebbene Gin abbia poche funzioni integrate, i middleware di estensione sviluppati dalla comunità di terze parti sono molto ricchi.

Il middleware è essenzialmente ancora un handler di interfaccia

go
// HandlerFunc definisce l'handler usato dal middleware gin come valore di ritorno.
type HandlerFunc func(*Context)

In un certo senso, anche l'handler corrispondente a ogni richiesta è un middleware, ma è un middleware locale con un ambito molto piccolo.

go
func Default() *Engine {
   debugPrintWARNINGDefault()
   engine := New()
   engine.Use(Logger(), Recovery())
   return engine
}

Controlla il codice sorgente di gin, nella funzione Default, l'Engine predefinito restituito usa due middleware predefiniti Logger(), Recovery(), se non si desidera usare i middleware predefiniti è anche possibile usare gin.New() come alternativa.

Middleware Globale

Il middleware globale ha un ambito globale, tutte le richieste dell'intero sistema passeranno attraverso questo middleware.

go
func GlobalMiddleware() gin.HandlerFunc {
   return func(ctx *gin.Context) {
      fmt.Println("Middleware globale eseguito...")
   }
}

Crea prima una funzione closure per creare il middleware, poi registra il middleware globale tramite Engine.Use().

go
func main() {
   e := gin.Default()
   // Registra middleware globale
   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'

Output

[GIN-debug] Listening and serving HTTP on :8080
Middleware globale eseguito...
[GIN] 2022/12/21 - 11:57:52 | 200 |       538.9µs |             ::1 | GET      "/v1/hello"

Middleware Locale

Il middleware locale ha un ambito locale, solo le richieste locali del sistema passeranno attraverso questo middleware. Il middleware locale può essere registrato su una singola route, ma più spesso è registrato su un gruppo di route.

go
func main() {
   e := gin.Default()
   // Registra middleware globale
   e.Use(GlobalMiddleware())
   // Registra middleware locale gruppo route
   v1 := e.Group("/v1", LocalMiddleware())
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      // Registra middleware locale singola 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'

Output

Middleware globale eseguito...
Middleware locale eseguito
[GIN] 2022/12/21 - 12:05:03 | 200 |       999.9µs |             ::1 | POST     "/v2/update"

Principio Middleware

L'uso e la personalizzazione dei middleware in Gin è molto facile, il principio interno è anche relativamente semplice, per l'apprendimento successivo, è necessario comprendere semplicemente il principio interno. Il middleware in Gin usa in realtà il pattern Chain of Responsibility, Context mantiene un HandlersChain, essenzialmente un []HandlerFunc, e un index, il suo tipo di dato è int8. Nel metodo Engine.handlerHTTPRequest(c *Context), c'è un pezzo di codice che indica il processo di chiamata: dopo che gin ha trovato la route corrispondente nell'albero delle route, chiama il metodo Next().

go
if value.handlers != nil {
   // Assegna la catena di chiamata al Context
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   // Chiama middleware
   c.Next()
   c.writermem.WriteHeaderNow()
   return
}

La chiamata di Next() è la chiave, Next() itererà sugli HandlerFunc negli handlers della route e li eseguirà, a questo punto si può vedere che la funzione di index è registrare la posizione di chiamata del middleware. Tra questi, la funzione di interfaccia registrata per la route corrispondente è anche in handlers, questo è anche il motivo per cui è stato detto prima che l'interfaccia è anche un middleware.

go
func (c *Context) Next() {
   // +1 all'ingresso è per evitare la ricorsione infinita, il valore predefinito è -1
   c.index++
   for c.index < int8(len(c.handlers)) {
      // Esegue HandlerFunc
      c.handlers[c.index](c)
      // Esecuzione completata, index+1
      c.index++
   }
}

Modifica la logica di Hello(), per verificare se è davvero così

go
func Hello(c *gin.Context) {
   fmt.Println(c.HandlerNames())
}

Il risultato dell'output è

[github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.GlobalMiddleware.func1 main.LocalMiddleware.func1 main.Hello]

Si può vedere che l'ordine della catena di chiamata del middleware è: Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello, l'ultimo elemento della catena di chiamata è la vera funzione di interfaccia da eseguire, quelli precedenti sono tutti middleware.

TIP

Quando si registra una route locale, c'è la seguente asserzione

go
finalSize := len(group.Handlers) + len(handlers) // Numero totale middleware
assert1(finalSize < int(abortIndex), "too many handlers")

Dove abortIndex int8 = math.MaxInt8 >> 1 valore 63, ovvero quando si usa il sistema il numero di route registrate non deve superare 63.

Middleware Timer

Dopo aver compreso il principio del middleware sopra, è possibile scrivere un semplice middleware di statistica del tempo di richiesta.

go
func TimeMiddleware() gin.HandlerFunc {
   return func(context *gin.Context) {
      // Registra tempo inizio
      start := time.Now()
      // Esegue la catena di chiamata successiva
      context.Next()
      // Calcola intervallo di tempo
      duration := time.Since(start)
      // Output nanosecondi, per osservare i risultati
      fmt.Println("Tempo richiesta: ", duration.Nanoseconds())
   }
}

func main() {
  e := gin.Default()
  // Registra middleware globale, middleware timer
  e.Use(GlobalMiddleware(), TimeMiddleware())
  // Registra middleware locale gruppo route
  v1 := e.Group("/v1", LocalMiddleware())
  {
    v1.GET("/hello", Hello)
    v1.GET("/login", Login)
  }
  v2 := e.Group("/v2")
  {
    // Registra middleware locale singola 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'

Output

Tempo richiesta:  517600

Un semplice middleware timer è già stato scritto, in seguito è possibile scrivere alcuni middleware più pratici con le proprie esplorazioni.

Configurazione Servizio

Usare solo la configurazione predefinita è lungi dall'essere sufficiente, nella maggior parte dei casi è necessario modificare molte configurazioni del servizio per soddisfare i requisiti.

Configurazione Http

È possibile configurare creando un Server tramite net/http, Gin supporta anche l'uso di Gin come le 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())
}

Configurazione Risorse Statiche

Le risorse statiche in passato erano fondamentalmente una parte indispensabile del lato server, sebbene la percentuale di utilizzo si stia gradualmente riducendo ora, ci sono ancora un gran numero di sistemi che usano ancora un'architettura monolitica.

Gin fornisce tre metodi per caricare risorse statiche

go
// Carica una certa cartella statica
func (group *RouterGroup) Static(relativePath, root string) IRoutes

// Carica un certo fs
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes

// Carica un certo file statico
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes

TIP

relativePath è il percorso relativo mappato sull'URL web, root è il percorso effettivo del file nel progetto

Supponiamo che la directory del progetto sia la seguente

root
|
|-- static
|  |
|  |-- a.jpg
|  |
|  |-- favicon.ico
|
|-- view
  |
  |-- html
go
func main() {
   router := gin.Default()
   // Carica directory file statici
   router.Static("/static", "./static")
   // Carica directory file statici
   router.StaticFS("/view", http.Dir("view"))
   // Carica file statico
   router.StaticFile("/favicon", "./static/favicon.ico")

   router.Run(":8080")
}

Configurazione CORS

Gin non ha alcun trattamento per la configurazione CORS, è necessario scrivere autonomamente i middleware per implementare i requisiti corrispondenti, in realtà la difficoltà non è grande, chi conosce leggermente il protocollo HTTP è generalmente in grado di scriverlo, la logica è fondamentalmente quella.

go
func CorsMiddle() gin.HandlerFunc {
   return func(c *gin.Context) {
      method := c.Request.Method
      origin := c.Request.Header.Get("Origin")
      if origin != "" {
         // I server in produzione generalmente non riempiono *, dovrebbero riempire nomi dominio specifici
         c.Header("Access-Control-Allow-Origin", origin)
         // HTTP METHOD consentiti
         c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
         // Header richiesta consentiti
         c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
         // Header risposta accessibili al client
         c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
         // Se è necessario portare informazioni di autenticazione Credentials possono essere cookies, authorization headers o TLS client certificates
         // Quando impostato su true, Access-Control-Allow-Origin non può essere *
         c.Header("Access-Control-Allow-Credentials", "true")
      }
      // Consenti richiesta OPTION, ma non esegue metodi successivi
      if method == "OPTIONS" {
         c.AbortWithStatus(http.StatusNoContent)
      }
      // Consenti
      c.Next()
   }
}

Registra il middleware come middleware globale

Controllo Sessione

Nell'era attuale, i tre tipi popolari di controllo sessione Web sono in totale tre, cookie, session, JWT.

Le informazioni nel cookie sono memorizzate nel browser in forma di coppie chiave-valore, e i dati possono essere visti direttamente nel browser

Vantaggi:

  • Struttura semplice
  • Dati persistenti

Svantaggi:

  • Dimensione limitata
  • Memorizzazione in chiaro
  • Facile da attaccare CSRF
go
import (
    "fmt"

    "github.com/gin-gonic/gin"
)

func main() {

    router := gin.Default()

    router.GET("/cookie", func(c *gin.Context) {

         // Ottieni cookie corrispondente
        cookie, err := c.Cookie("gin_cookie")

        if err != nil {
            cookie = "NotSet"
            // Imposta cookie parametri: key, val, tempo esistenza, directory, dominio, se consentire ad altri di accedere al cookie tramite js, solo http
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

        fmt.Printf("Cookie value: %s \n", cookie)
    })

    router.Run()
}

Il cookie puro era usato più spesso cinque o sei anni fa, ma l'autore generalmente usa raramente il cookie puro per il controllo sessione, farlo è davvero poco sicuro.

Session

La session è memorizzata sul server, quindi viene inviato un cookie memorizzato nel browser, il cookie memorizza il session_id, in seguito ogni richiesta il server può ottenere le informazioni della session tramite il session_id

Vantaggi:

  • Memorizzato sul lato server, aumenta la sicurezza, facile da gestire

Svantaggi:

  • Memorizzato sul lato server, aumenta il costo del server, riduce le prestazioni
  • Basato sul riconoscimento cookie, non sicuro
  • Le informazioni di autenticazione non sono sincronizzate in situazioni distribuite

Session e Cookie sono inseparabili, ogni volta che si usa Session, per default si usa Cookie. Gin non supporta Session per default, perché Cookie è contenuto nel protocollo Http, ma Session no, ma ci sono middleware di terze parti che supportano, installa le dipendenze, repository: gin-contrib/sessions: Gin middleware for session management (github.com)

go get github.com/gin-contrib/sessions

Supporta cookie, Redis, MongoDB, GORM, PostgreSQL

go
func main() {
   r := gin.Default()
   // Crea motore di archiviazione basato su Cookie
   store := cookie.NewStore([]byte("secret"))
   // Imposta middleware Session, mysession è il nome della session, anche il nome del cookie
   r.Use(sessions.Sessions("mysession", store))
   r.GET("/incr", func(c *gin.Context) {
      // Inizializza session
      session := sessions.Default(c)
      var count int
      // Ottieni valore
      v := session.Get("count")
      if v == nil {
         count = 0
      } else {
         count = v.(int)
         count++
      }
      // Imposta
      session.Set("count", count)
      // Salva
      session.Save()
      c.JSON(200, gin.H{"count": count})
   })
   r.Run(":8000")
}

Generalmente non si consiglia di archiviare Sesison tramite Cookie, si consiglia di usare Redis, altri esempi si prega di andare al repository ufficiale per comprendere.

JWT

Vantaggi:

  • Basato su JSON, multipiattaforma e linguaggio
  • Può memorizzare informazioni non sensibili
  • Occupa poco, facile da trasmettere
  • Il server non ha bisogno di memorizzare, favorevole all'espansione distribuita

Svantaggi:

  • Problema di aggiornamento Token
  • Una volta emesso non può essere controllato attivamente

Dalla rivoluzione frontend, i programmatori frontend non sono più solo "quelli che scrivono pagine", la tendenza alla separazione tra frontend e backend è diventata sempre più forte, JWT è il più adatto per la separazione tra frontend e backend e i sistemi distribuiti per il controllo sessione, ha grandi vantaggi naturali. Considerando che JWT si è completamente staccato dal contenuto di Gin e non ha alcun supporto middleware, perché JWT non è limitato a nessun framework o linguaggio, qui non verrà spiegata in dettaglio, è possibile andare a un altro documento: JWT

Gestione Log

Il middleware log predefinito usato da Gin usa os.Stdout, ha solo le funzioni più basilari, dopotutto Gin si concentra solo sul servizio Web, nella maggior parte dei casi si dovrebbe usare un framework log più maturo, ma questo non rientra nell'ambito di questo capitolo, e Gin ha un'altissima estensibilità, può integrare facilmente altri framework, qui discuteremo solo il suo servizio log integrato.

Colore Console

go
gin.DisableConsoleColor() // Disattiva colore log console

Oltre allo sviluppo, nella maggior parte dei casi non si consiglia di attivare questa opzione

Log Scritti su File

go
func main() {
  e := gin.Default()
    // Disattiva colore console
  gin.DisableConsoleColor()
    // Crea due file log
  log1, _ := os.Create("info1.log")
  log2, _ := os.Create("info2.log")
    // Registra contemporaneamente in due file log
  gin.DefaultWriter = io.MultiWriter(log1, log2)
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

Il log integrato di gin supporta la scrittura su più file, ma il contenuto è lo stesso, non è molto comodo da usare, e non scriverà i log delle richieste nei file.

go
func main() {
  router := gin.New()
  // Il middleware LoggerWithFormatter scriverà i log in gin.DefaultWriter
  // Per default gin.DefaultWriter = os.Stdout
  router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        //TODO logica scrittura file corrispondente
        ......
    // Output formato personalizzato
    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")
}

Tramite middleware personalizzato, è possibile realizzare la scrittura dei log su file

Formato Log Debug Route

Qui si modifica solo il log che output le informazioni delle route all'avvio

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"))
}

Output

2022/12/21 17:19:13 Route GET /hello main.Hello 3

Conclusione: Gin è il framework Web più facile da imparare nel linguaggio Go, perché Gin ha davvero raggiunto la minimizzazione delle responsabilità, è solo responsabile del servizio Web, altre logiche di autenticazione, cache dati e altre funzioni sono lasciate agli sviluppatori per completarle autonomamente, rispetto a quei framework grandi e completi, Gin leggero e conciso è più adatto e dovrebbe essere imparato per i principianti, perché Gin non impone l'uso di una certa specifica, come costruire il progetto, quale struttura adottare devono essere considerati autonomamente, per i principianti è più in grado di esercitare le capacità.

Golang by www.golangdev.cn edit