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.
go get -u github.com/gin-gonic/ginImport
import "github.com/gin-gonic/gin"Inizio Rapido
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
GET localhost:8080/pingRisposta
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.
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
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'username is jack
userid is 001Esempio due
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'filepath is /img/fruit.pngParametri URL
I parametri URL tradizionali, il formato è /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)
}Esempio uno
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'username is jack
userid is 001Esempio due
curl --location --request GET '127.0.0.1:8080/findUser'username is defaultUser
userid isParametri Form
Il tipo di contenuto del form è generalmente 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"])
}Esempio uno: Usa 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]Il metodo PostForm analizza per default i form di tipo application/x-www-form-urlencoded e multipart/form-data.
Esempio due: Usa 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]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.
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
c.MustBindWith(obj, binding.JSON) //json
c.MustBindWith(obj, binding.XML) //xmlI tipi di binding supportati da gin sono i seguenti:
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
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
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
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'login successfully !Binding Dati URL
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:
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.
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
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' tagTIP
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
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
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
<!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:
// 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) boolPer 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.
// 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() *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 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
curl --location --request GET 'http://localhost:8080/hello'Output
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
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
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.
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 GMTNon pensi che sia troppo semplice, potresti non usare il metodo del framework, scrivere il processo da solo
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.
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.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroupQuando 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.
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
func (engine *Engine) NoRoute(handlers ...HandlerFunc)Prendiamo l'esempio precedente
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
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.
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 allowedConfigurazione completata
Reindirizzamento
Il reindirizzamento in gin è molto semplice, basta chiamare il metodo 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/'Output
helloMiddleware
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
// 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.
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.
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().
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.
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().
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.
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ì
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
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.
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: 517600Un 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.
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
// 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) IRoutesTIP
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
|
|-- htmlfunc 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.
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.
Cookie
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
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/sessionsSupporta cookie, Redis, MongoDB, GORM, PostgreSQL
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
gin.DisableConsoleColor() // Disattiva colore log consoleOltre allo sviluppo, nella maggior parte dei casi non si consiglia di attivare questa opzione
Log Scritti su File
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.
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
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 3Conclusione: 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à.
