Skip to content

Gin

Documentación oficial: Gin Web Framework (gin-gonic.com)

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

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

Introducción

Gin es un framework web escrito en Go (Golang). Tiene una API similar a martini, pero con mucho mejor rendimiento, gracias a httprouter, la velocidad es 40 veces mayor. Si necesitas rendimiento y buena productividad, definitivamente te gustará Gin. Gin en comparación con Iris y Beego, tiende a ser un framework más ligero, solo se encarga de la parte web, busca un rendimiento extremo de enrutamiento, las funciones pueden no ser tan completas, pero gana en ser ligero y fácil de expandir, esta es también su ventaja. Por lo tanto, entre todos los frameworks web, Gin es el más fácil de aprender y usar.

Características

  • Rápido: Enrutamiento basado en árbol Radix, pequeña huella de memoria. Sin reflexión. Rendimiento de API predecible.
  • Soporte de middleware: Las solicitudes HTTP entrantes pueden ser manejadas por una serie de middleware y operaciones finales. Por ejemplo: Logger, Authorization, GZIP, operación final DB.
  • Manejo de fallos: Gin puede capturar un panic que ocurra en una solicitud HTTP y recuperarlo. De esta manera, tu servidor siempre estará disponible.
  • Validación JSON: Gin puede analizar y validar el JSON de la solicitud, por ejemplo, verificar la presencia de valores requeridos.
  • Grupos de rutas: Mejor organización de rutas. ¿Necesitas autorización, diferentes versiones de API?... Además, estos grupos se pueden anidar sin restricciones sin degradar el rendimiento.
  • Gestión de errores: Gin proporciona una forma conveniente de recopilar todos los errores que ocurren durante las solicitudes HTTP. Finalmente, el middleware puede escribirlos en archivos de registro, base de datos y enviarlos a través de la red.
  • Renderizado incorporado: Gin proporciona una API fácil de usar para renderizar JSON, XML y HTML.
  • Extensible: Crear un nuevo middleware es muy simple

Instalación

Hasta la fecha 2022/11/22, la versión mínima de Go soportada por gin es 1.16, se recomienda usar go mod para gestionar las dependencias del proyecto.

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

Importar

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

Inicio rápido

go
package main

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

func main() {
   engine := gin.Default() // crear motor gin
   engine.GET("/ping", func(context *gin.Context) {
      context.JSON(http.StatusOK, gin.H{
         "message": "pong",
      })
   })
   engine.Run() // iniciar servidor, escucha por defecto en localhost:8080
}

Solicitar URL

http
GET localhost:8080/ping

Respuesta

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

Documentación

En realidad, la documentación oficial de Gin no tiene muchos tutoriales, la mayoría son solo introducciones, uso básico y algunos ejemplos, pero bajo la organización gin-gonic/, hay un repositorio gin-gonic/examples, este es un repositorio de ejemplos de gin mantenido por la comunidad. Todo está en inglés, el tiempo de actualización no es particularmente frecuente, el autor también está aprendiendo lentamente el framework gin desde aquí.

Repositorio de ejemplos: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

TIP

Antes de comenzar, se recomienda leer HttpRouter: HttpRouter

Análisis de parámetros

El análisis de parámetros en gin soporta tres métodos en total: parámetros de ruta, parámetros URL, parámetros de formulario, a continuación se explica cada uno con ejemplos de código, es simple y fácil de entender.

Parámetros de ruta

Los parámetros de ruta en realidad encapsulan la funcionalidad de análisis de parámetros de HttpRouter, el método de uso es básicamente el mismo 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"))
}

// Ejemplo de parámetros con nombre
func FindUser(c *gin.Context) {
   username := c.Param("username")
   userid := c.Param("userid")
   c.String(http.StatusOK, "username es %s\n userid es %s", username, userid)
}

// Ejemplo de parámetros de ruta
func UserPage(c *gin.Context) {
   filepath := c.Param("filepath")
   c.String(http.StatusOK, "filepath es  %s", filepath)
}

Ejemplo uno

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

Ejemplo dos

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

Parámetros URL

Los parámetros URL tradicionales, el formato es /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 es %s\nuserid es %s", username, userid)
}

Ejemplo uno

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

Ejemplo dos

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

Parámetros de formulario

El tipo de contenido del formulario generalmente tiene 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, "registro exitoso, tu username es [%s],password es [%s]", username, password)
}

func UpdateUser(c *gin.Context) {
  var form map[string]string
  c.ShouldBind(&form)
  c.String(http.StatusOK, "actualización exitosa, tu username es [%s],password es [%s]", form["username"], form["password"])
}

Ejemplo uno: usar form-data

bash
curl --location --request POST '127.0.0.1:8080/register' \
--form 'username="jack"' \
--form 'password="123456"'
registro exitoso, tu username es [jack],password es [123456]

El método PostForm analiza por defecto formularios de tipo application/x-www-form-urlencoded y multipart/form-data.

Ejemplo dos: usar json

bash
curl --location --request POST '127.0.0.1:8080/update' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"username",
    "password":"123456"
}'
actualización exitosa, tu username es [username],password es [123456]

Análisis de datos

En la mayoría de los casos, usaremos una estructura para transportar datos, en lugar de analizar parámetros directamente. En gin, los métodos principales para enlace de datos son Bind() y ShouldBind(), la diferencia entre ellos es que el primero internamente llama directamente a ShouldBind(), por supuesto, cuando devuelve err, realizará una respuesta 400, el segundo no lo hará. Si deseas realizar un manejo de errores más flexible, se recomienda elegir el segundo. Estas dos funciones inferirán automáticamente según el content-type de la solicitud qué método de análisis usar.

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

Si deseas seleccionar manualmente, puedes usar BindWith() y ShouldBindWith(), por ejemplo

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

Los tipos de enlace soportados por gin son las siguientes implementaciones:

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

Ejemplo

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
    // usar ShouldBind para que gin infiera automáticamente
  if c.ShouldBind(&login) == nil && login.Password != "" && login.Username != "" {
    c.String(http.StatusOK, "inicio de sesión exitoso !")
  } else {
    c.String(http.StatusBadRequest, "inicio de sesión fallido !")
  }
  fmt.Println(login)
}

Enlace de datos Json

bash
curl --location --request POST '127.0.0.1:8080/loginWithJSON' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"root",
    "password":"root"
}'
inicio de sesión exitoso !

Enlace de datos de formulario

go
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'
inicio de sesión exitoso !

Enlace de datos URL

go
curl --location --request GET '127.0.0.1:8080/loginWithQuery/root/root'
inicio de sesión fallido !

Aquí ocurrirá un error, porque el content-type de salida es una cadena vacía, no se puede inferir cómo se deben analizar los datos. Por lo tanto, cuando se usan parámetros URL, debemos especificar manualmente el método de análisis, por ejemplo:

go
if err := c.ShouldBindUri(&login); err == nil && login.Password != "" && login.Username != "" {
   c.String(http.StatusOK, "inicio de sesión exitoso !")
} else {
   fmt.Println(err)
   c.String(http.StatusBadRequest, "inicio de sesión fallido !")
}

Múltiples enlaces

Generalmente los métodos se llaman a través del método c.Request.Body para enlazar datos, pero no se puede llamar a este método múltiples veces, por ejemplo c.ShouldBind, no es reutilizable, si deseas enlazar múltiples veces, puedes usar

c.ShouldBindBodyWith.

go
func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // leer c.Request.Body y almacenar el resultado en el contexto.
  if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
    c.String(http.StatusOK, `el body debe ser formA`)
  // en este momento, reutilizar el body almacenado en el contexto.
  }
  if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `el body debe ser formB JSON`)
  // puede aceptar otros formatos
  }
  if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
    c.String(http.StatusOK, `el body debe ser formB XML`)
  }
}

TIP

c.ShouldBindBodyWith almacenará el body en el contexto antes del enlace. Esto tendrá un ligero impacto en el rendimiento, si se puede completar el enlace con una sola llamada, entonces no uses este método. Solo algunos formatos necesitan esta función, como JSON, XML, MsgPack, ProtoBuf. Para otros formatos, como Query, Form, FormPost, FormMultipart se puede llamar a c.ShouldBind() múltiples veces sin causar ninguna pérdida de rendimiento.

Validación de datos

La herramienta de validación incorporada en gin es en realidad github.com/go-playground/validator/v10, el método de uso es casi el mismo, Validator

Ejemplo 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, "usuario inválido,%v", err)
   }
}

Prueba

curl --location --request POST 'http://localhost:8080/register' \
--header 'Content-Type: application/json' \
--data-raw '{
    "username":"jack1"

}'

Salida

usuario inválido,Key: 'LoginUser.Password' Error:Field validation for 'Password' failed on the 'required' tag

TIP

Un punto a tener en cuenta es que la tag de validación de validator en gin es binding, mientras que la tag de validación al usar validator solo es validator

Respuesta de datos

La respuesta de datos es el último paso que se debe hacer en el manejo de interfaces, el backend devuelve todos los datos procesados al llamador a través del protocolo HTTP, gin proporciona un rico soporte incorporado para la respuesta de datos, el uso es simple y claro, muy fácil de usar.

Ejemplo simple

go
func Hello(c *gin.Context) {
    // devolver datos en formato de cadena pura, http.StatusOK representa el código de estado 200, los datos son "Hello world !"
  c.String(http.StatusOK, "Hello world !")
}

Renderizado HTML

TIP

Al cargar archivos, la ruta raíz predeterminada es la ruta del proyecto, es decir, la ruta donde se encuentra el archivo go.mod, index.html en los siguientes ejemplos se encuentra en index.html en la ruta raíz, pero en general estos archivos de plantilla no se colocan en la ruta raíz, sino que se almacenan en carpetas de recursos estáticos

go
func main() {
   e := gin.Default()
    // cargar archivo HTML, también se puede usar 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{})
}

Prueba

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

Respuesta

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>

Respuesta rápida

Anteriormente se usaba a menudo el método context.String() para responder datos, este es el método de respuesta más original, devuelve directamente una cadena, gin también tiene incorporados muchos métodos de respuesta rápida, por ejemplo:

go
// usar Render para escribir encabezado de respuesta y realizar renderizado de datos
func (c *Context) Render(code int, r render.Render)

// renderizar una plantilla HTML, name es la ruta html, obj es el contenido
func (c *Context) HTML(code int, name string, obj any)

// renderizado de datos con cadena JSON con sangría embellecida, generalmente no se recomienda usar este método, porque causará más consumo de transmisión.
func (c *Context) IndentedJSON(code int, obj any)

// JSON seguro, puede prevenir el secuestro de JSON, más detalles: https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)

// renderizado mediante JSONP
func (c *Context) JSONP(code int, obj any)

// renderizado mediante JSON
func (c *Context) JSON(code int, obj any)

// renderizado mediante JSON, convertirá el código unicode a código ASCII
func (c *Context) AsciiJSON(code int, obj any)

// renderizado mediante JSON, no escapará caracteres especiales HTML
func (c *Context) PureJSON(code int, obj any)

// renderizado mediante XML
func (c *Context) XML(code int, obj any)

// renderizado mediante YML
func (c *Context) YAML(code int, obj any)

// renderizado mediante TOML
func (c *Context) TOML(code int, obj interface{})

// renderizado mediante ProtoBuf
func (c *Context) ProtoBuf(code int, obj any)

// renderizado mediante String
func (c *Context) String(code int, format string, values ...any)

// redirigir a una ubicación específica
func (c *Context) Redirect(code int, location string)

// escribir data en el flujo de respuesta
func (c *Context) Data(code int, contentType string, data []byte)

// leer flujo a través de reader y escribir en el flujo de respuesta
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)

// escribir archivo en el flujo de respuesta de manera eficiente
func (c *Context) File(filepath string)

// escribir flujo de archivo desde fs en el flujo de respuesta de manera eficiente
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)

// escribir flujo de archivo desde fs en el flujo de respuesta de manera eficiente, y el cliente lo descargará con el nombre de archivo especificado
func (c *Context) FileAttachment(filepath, filename string)

// escribir flujo de push del servidor en el flujo de respuesta
func (c *Context) SSEvent(name string, message any)

// enviar una respuesta de flujo y devolver un valor booleano, para determinar si el cliente se desconectó en medio del flujo
func (c *Context) Stream(step func(w io.Writer) bool) bool

Para la mayoría de las aplicaciones, lo que más se usa es context.JSON, los demás son relativamente menos utilizados, aquí no se darán ejemplos de demostración, porque son bastante simples y fáciles de entender, casi todos son llamadas directas.

Procesamiento asíncrono

En gin, el procesamiento asíncrono necesita usarse junto con goroutine, es muy simple de usar.

go
// copy devuelve una copia del Context actual para usar de forma segura fuera del ámbito del Context actual, se puede usar para pasar a un 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 sub-goroutine debe usar la copia del Context, no debe usar el Context original
    log.Println("función de procesamiento asíncrono: ", ctx.HandlerNames())
  }()
  log.Println("función de procesamiento de interfaz: ", c.HandlerNames())
  c.String(http.StatusOK, "hello")
}

Prueba

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

Salida

go
2022/12/21 13:33:47 función de procesamiento asíncrono:  []
2022/12/21 13:33:47 función de procesamiento de interfaz:  [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"

Se puede ver que las salidas de ambos son diferentes, la copia al copiar, por razones de seguridad, eliminó muchos valores de elementos.

Transferencia de archivos

La transferencia de archivos es una función indispensable en las aplicaciones web, el soporte de gin para esto también está encapsulado de manera muy simple, pero en esencia es casi lo mismo que usar el net/http nativo. El proceso es leer el flujo de archivos del cuerpo de la solicitud y luego guardarlo localmente.

Carga de archivo único

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

func uploadFile(ctx *gin.Context) {
  // obtener archivo
  file, err := ctx.FormFile("file")
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // guardar localmente
  err = ctx.SaveUploadedFile(file, "./"+file.Filename)
  if err != nil {
    ctx.String(http.StatusBadRequest, "%+v", err)
    return
  }
  // devolver resultado
  ctx.String(http.StatusOK, "carga %s tamaño:%d byte exitosamente!", file.Filename, file.Size)
}

Prueba

curl --location --request POST 'http://localhost:8080/upload' \
--form 'file=@"/C:/Users/user/Pictures/Camera Roll/a.jpg"'

Resultado

carga a.jpg tamaño:1424 byte exitosamente!

TIP

En general, el Method para cargar archivos especificará usar POST, algunas empresas pueden preferir usar PUT, el primero es una solicitud HTTP simple, el segundo es una solicitud HTTP compleja, no se detallará la diferencia específica, si se usa el segundo, especialmente en proyectos de separación de frontend y backend, se necesita el manejo de CORS correspondiente, y la configuración predeterminada de Gin no soporta CORS configuración de CORS.

Carga de múltiples archivos

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

func uploadFiles(ctx *gin.Context) {
  // obtener formulario multipart analizado por gin
  form, _ := ctx.MultipartForm()
  // obtener lista de archivos correspondiente según la clave
  files := form.File["files"]
  // iterar lista de archivos, guardar localmente
  for _, file := range files {
    err := ctx.SaveUploadedFile(file, "./"+file.Filename)
    if err != nil {
      ctx.String(http.StatusBadRequest, "carga fallida")
      return
    }
  }
  // devolver resultado
  ctx.String(http.StatusOK, "carga %d archivos exitosamente!", len(files))
}

Prueba

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

Salida

carga 3 archivos exitosamente!

Descarga de archivos

Sobre la parte de descarga de archivos, Gin encapsula nuevamente la API de la biblioteca estándar original, haciendo que la descarga de archivos sea extremadamente 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) {
    // obtener nombre de archivo
  filename := ctx.Param("filename")
    // devolver archivo correspondiente
  ctx.FileAttachment(filename, filename)
}

Prueba

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

Resultado

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

¿No crees que es demasiado simple?,不妨 no uses el método del framework, escribe tú mismo el proceso

go
func download(ctx *gin.Context) {
   // obtener parámetro
   filename := ctx.Param("filename")

   // objeto de respuesta y objeto de solicitud
   response, request := ctx.Writer, ctx.Request
   // escribir encabezado de respuesta
   // response.Header().Set("Content-Type", "application/octet-stream")  transmitir archivo como flujo binario
   response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // escape seguro del nombre de archivo
   response.Header().Set("Content-Transfer-Encoding", "binary")                                            // codificación de transmisión
   http.ServeFile(response, request, filename)
}

En realidad net/http ya está suficientemente bien encapsulado

TIP

Puedes usar Engine.MaxMultipartMemory para establecer la memoria máxima para la transferencia de archivos, el valor predeterminado es 32 << 20 // 32 MB

Gestión de rutas

La gestión de rutas es una parte muy importante de un sistema, es necesario asegurarse de que cada solicitud se pueda mapear correctamente a la función correspondiente.

Grupos de rutas

Crear un grupo de rutas es clasificar las interfaces, las interfaces de diferentes categorías corresponden a diferentes funciones, también es más fácil de gestionar.

go
func Hello(c *gin.Context) {

}

func Login(c *gin.Context) {

}

func Update(c *gin.Context) {

}

func Delete(c *gin.Context) {

}

Supongamos que tenemos las cuatro interfaces anteriores, temporalmente no importa su implementación interna, Hello, Login son un grupo, Update, Delete son un grupo.

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

Al crear un grupo, también podemos registrar un manejador para la ruta raíz del grupo, pero la mayoría de las veces no se hace así.

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

Los dividimos en dos grupos v1, v2, las llaves {} son solo para estandarizar, indican que los manejadores registrados entre llaves pertenecen al mismo grupo de rutas, no tienen ningún efecto funcional. Del mismo modo, gin también soporta grupos anidados, el método es el mismo que el ejemplo anterior, aquí no se demostrará más.

Ruta 404

La estructura Engine en gin proporciona un método NoRoute, para establecer cómo manejar cuando la URL de acceso no existe, los desarrolladores pueden escribir la lógica en este método, para que se llame automáticamente cuando no se encuentra la ruta, por defecto devolverá el código de estado 404

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

Tomemos el ejemplo anterior

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)
   }
   // registrar manejador
   e.NoRoute(func(context *gin.Context) { // esto es solo una demostración, no devuelvas código HTML directamente en producción
      context.String(http.StatusNotFound, "<h1>404 Página no encontrada</h1>")
   })
   log.Fatalln(e.Run(":8080"))
}

Enviar una solicitud aleatoria

curl --location --request GET 'http://localhost:8080/'
<h1>404 Página no encontrada</h1>

Ruta 405

En el código de estado Http, 405 significa que el tipo de método de solicitud actual no está permitido, gin proporciona el siguiente método

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

para registrar un manejador, para que se llame automáticamente cuando ocurra, la premisa es establecer Engine.HandleMethodNotAllowed = true.

go
func main() {
   e := gin.Default()
   // necesita establecerlo en 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 Página no encontrada</h1>")
   })
   // registrar manejador
   e.NoMethod(func(context *gin.Context) {
      context.String(http.StatusMethodNotAllowed, "método no permitido")
   })
   log.Fatalln(e.Run(":8080"))
}

Después de configurar, el header predeterminado de gin no soporta solicitudes OPTION, probemos

curl --location --request OPTIONS 'http://localhost:8080/v2/delete'
método no permitido

Con esto se configura exitosamente

Redirección

La redirección en gin es muy simple, solo llama al método 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")
}

Prueba

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

Salida

hello

Middleware

Gin es muy ligero y flexible, tiene una escalabilidad muy alta, y el soporte para middleware también es muy amigable. En Gin, todas las solicitudes de interfaz pasan por middleware, a través del middleware, los desarrolladores pueden implementar muchas funciones y lógicas personalizadas, aunque Gin tiene muy pocas funciones incorporadas, los middleware de extensión desarrollados por la comunidad de terceros son muy ricos.

El middleware en esencia es todavía un manejador de interfaz

go
// HandlerFunc define el manejador usado por el middleware de gin como valor de retorno.
type HandlerFunc func(*Context)

En cierto sentido, el manejador correspondiente a cada solicitud también es middleware, pero es un middleware local de alcance muy pequeño.

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

Al ver el código fuente de gin, en la función Default, el Engine predeterminado devuelto usa dos middlewares predeterminados Logger(), Recovery(), si no quieres usar los middlewares predeterminados también puedes usar gin.New() en su lugar.

Middleware global

El middleware global es de alcance global, todas las solicitudes de todo el sistema pasarán por este middleware.

go
func GlobalMiddleware() gin.HandlerFunc {
   return func(ctx *gin.Context) {
      fmt.Println("middleware global ejecutado...")
   }
}

Primero crea una función de cierre para crear el middleware, luego registra el middleware global a través de Engine.Use().

go
func main() {
   e := gin.Default()
   // registrar 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"))
}

Prueba

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

Salida

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

Middleware local

El middleware local es de alcance local, las solicitudes locales del sistema pasarán por este middleware. El middleware local se puede registrar en una sola ruta, pero más a menudo se registra en un grupo de rutas.

go
func main() {
   e := gin.Default()
   // registrar middleware global
   e.Use(GlobalMiddleware())
   // registrar middleware local de grupo de rutas
   v1 := e.Group("/v1", LocalMiddleware())
   {
      v1.GET("/hello", Hello)
      v1.GET("/login", Login)
   }
   v2 := e.Group("/v2")
   {
      // registrar middleware local de ruta única
      v2.POST("/update", LocalMiddleware(), Update)
      v2.DELETE("/delete", Delete)
   }
   log.Fatalln(e.Run(":8080"))
}

Prueba

curl --location --request POST 'http://localhost:8080/v2/update'

Salida

middleware global ejecutado...
middleware local ejecutado
[GIN] 2022/12/21 - 12:05:03 | 200 |       999.9µs |             ::1 | POST     "/v2/update"

Principio del middleware

El uso y personalización de middleware en Gin es muy fácil, el principio interno también es relativamente simple, para el aprendizaje posterior, es necesario entender simplemente el principio interno. El middleware en Gin en realidad usa el patrón de responsabilidad en cadena, Context mantiene un HandlersChain, que en esencia es un []HandlerFunc, y un index, su tipo de dato es int8. En el método Engine.handlerHTTPRequest(c *Context), hay un código que indica el proceso de llamada: después de que gin encuentra la ruta correspondiente en el árbol de rutas, llama al método Next().

go
if value.handlers != nil {
   // asignar la cadena de llamada al Context
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   // llamar al middleware
   c.Next()
   c.writermem.WriteHeaderNow()
   return
}

La llamada de Next() es la clave, Next() iterará sobre los HandlerFunc en los handlers de la ruta y los ejecutará, en este momento se puede ver que la función del index es registrar la posición de llamada del middleware. Entre ellos, la función de interfaz registrada para la ruta correspondiente también está en handlers, esta es también la razón por la que se dijo antes que la interfaz también es un middleware.

go
func (c *Context) Next() {
   // +1 al entrar es para evitar bucle recursivo infinito, el valor predeterminado es -1
   c.index++
   for c.index < int8(len(c.handlers)) {
      // ejecutar HandlerFunc
      c.handlers[c.index](c)
      // ejecución completada, index+1
      c.index++
   }
}

Modifica la lógica de Hello() para verificar si es realmente así

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

El resultado de salida es

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

Se puede ver que el orden de la cadena de llamada del middleware es: Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello, el último elemento de la cadena de llamada es la función de interfaz que realmente se ejecutará, los anteriores son todos middlewares.

TIP

Al registrar rutas locales, hay la siguiente afirmación

go
finalSize := len(group.Handlers) + len(handlers) // número total de middlewares
assert1(finalSize < int(abortIndex), "demasiados handlers")

Donde abortIndex int8 = math.MaxInt8 >> 1 valor 63, es decir, al usar el sistema, el número de rutas registradas no debe exceder 63.

Middleware de temporizador

Después de conocer el principio del middleware anterior, se puede escribir un middleware simple de estadísticas de tiempo de solicitud.

go
func TimeMiddleware() gin.HandlerFunc {
   return func(context *gin.Context) {
      // registrar tiempo de inicio
      start := time.Now()
      // ejecutar la cadena de llamada posterior
      context.Next()
      // calcular intervalo de tiempo
      duration := time.Since(start)
      // output en nanosegundos, para observar los resultados
      fmt.Println("tiempo de solicitud: ", duration.Nanoseconds())
   }
}

func main() {
  e := gin.Default()
  // registrar middleware global, middleware de temporizador
  e.Use(GlobalMiddleware(), TimeMiddleware())
  // registrar middleware local de grupo de rutas
  v1 := e.Group("/v1", LocalMiddleware())
  {
    v1.GET("/hello", Hello)
    v1.GET("/login", Login)
  }
  v2 := e.Group("/v2")
  {
    // registrar middleware local de ruta única
    v2.POST("/update", LocalMiddleware(), Update)
    v2.DELETE("/delete", Delete)
  }
  log.Fatalln(e.Run(":8080"))
}

Prueba

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

Salida

tiempo de solicitud:  517600

Un middleware de temporizador simple ya ha sido escrito, posteriormente puedes escribir algunos middlewares más prácticos según tu propia exploración.

Configuración del servicio

Usar solo la configuración predeterminada está lejos de ser suficiente, en la mayoría de los casos es necesario modificar mucha configuración del servicio para cumplir con los requisitos.

Configuración Http

Puedes crear Server a través de net/http para configurar, Gin también soporta usar Gin como la API nativa.

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

Configuración de recursos estáticos

Los recursos estáticos en el pasado eran básicamente una parte indispensable del lado del servidor, aunque en la actualidad el porcentaje de uso está disminuyendo gradualmente, todavía hay una gran cantidad de sistemas que todavía usan arquitectura monolítica.

Gin proporciona tres métodos para cargar recursos estáticos

go
// cargar una carpeta estática específica
func (group *RouterGroup) Static(relativePath, root string) IRoutes

// cargar un fs específico
func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes

// cargar un archivo estático específico
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes

TIP

relativePath es la ruta relativa mapeada a la URL de la página web, root es la ruta real del archivo en el proyecto

Supongamos que el directorio del proyecto es el siguiente

root
|
|-- static
|  |
|  |-- a.jpg
|  |
|  |-- favicon.ico
|
|-- view
  |
  |-- html
go
func main() {
   router := gin.Default()
   // cargar directorio de archivos estáticos
   router.Static("/static", "./static")
   // cargar directorio de archivos estáticos
   router.StaticFS("/view", http.Dir("view"))
   // cargar archivo estático
   router.StaticFile("/favicon", "./static/favicon.ico")

   router.Run(":8080")
}

Configuración de CORS

Gin no tiene ningún manejo para la configuración de CORS, es necesario escribir middleware personalmente para implementar los requisitos correspondientes, en realidad la dificultad no es grande, las personas que están familiarizadas con el protocolo HTTP generalmente pueden escribirlo, la lógica es básicamente la misma.

go
func CorsMiddle() gin.HandlerFunc {
   return func(c *gin.Context) {
      method := c.Request.Method
      origin := c.Request.Header.Get("Origin")
      if origin != "" {
         // en entorno de producción, los servidores generalmente no llenan *, se debe llenar con nombre de dominio específico
         c.Header("Access-Control-Allow-Origin", origin)
         // HTTP METHOD permitidos
         c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
         // Encabezados de solicitud permitidos
         c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
         // Encabezados de respuesta a los que el cliente puede acceder
         c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
         // Si se necesita información de autenticación Credentials puede ser cookies, authorization headers o TLS client certificates
         // cuando se establece en true, Access-Control-Allow-Origin no puede ser *
         c.Header("Access-Control-Allow-Credentials", "true")
      }
      // permitir solicitud OPTION, pero no ejecutar métodos posteriores
      if method == "OPTIONS" {
         c.AbortWithStatus(http.StatusNoContent)
      }
      // permitir
      c.Next()
   }
}

Registra el middleware como middleware global y listo

Control de sesión

En la era actual, los tres tipos populares de control de sesiones web son cookie, session, JWT.

La información en cookie se almacena en el navegador en forma de pares clave-valor, y los datos se pueden ver directamente en el navegador

Ventajas:

  • Estructura simple
  • Datos persistentes

Desventajas:

  • Tamaño limitado

  • Almacenamiento en texto plano

  • Fácil de atacar por CSRF

go
import (
    "fmt"

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

func main() {

    router := gin.Default()

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

         // obtener cookie correspondiente
        cookie, err := c.Cookie("gin_cookie")

        if err != nil {
            cookie = "NotSet"
            // establecer cookie  parámetros: key, val, tiempo de existencia, directorio, dominio, si permite que otros accedan a la cookie a través de js, solo http
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

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

    router.Run()
}

La cookie pura se usaba más hace cinco o seis años, pero el autor generalmente rara vez usa cookie pura para el control de sesiones, hacer esto确实 no es muy seguro.

Session

La session se almacena en el servidor, luego se envía una cookie almacenada en el navegador, la cookie almacena el session_id, después de eso cada solicitud el servidor puede obtener la información de session correspondiente a través del session_id

Ventajas:

  • Almacenado en el lado del servidor, aumenta la seguridad, fácil de gestionar

Desventajas:

  • Almacenado en el lado del servidor, aumenta la carga del servidor, reduce el rendimiento
  • Basado en identificación de cookie, no seguro
  • La información de autenticación no se sincroniza en situaciones distribuidas

Session y Cookie son inseparables, cada vez que se usa Session, por defecto se usa Cookie. Gin no soporta Session por defecto, porque Cookie es contenido del protocolo Http, pero Session no, sin embargo hay middleware de terceros que lo soporta, instala la dependencia, repositorio: gin-contrib/sessions: Gin middleware for session management (github.com)

go get github.com/gin-contrib/sessions

Soporta cookie, Redis, MongoDB, GORM, PostgreSQL

go
func main() {
   r := gin.Default()
   // crear motor de almacenamiento basado en Cookie
   store := cookie.NewStore([]byte("secret"))
   // establecer middleware Session, mysession es el nombre de session, también es el nombre de cookie
   r.Use(sessions.Sessions("mysession", store))
   r.GET("/incr", func(c *gin.Context) {
      // inicializar session
      session := sessions.Default(c)
      var count int
      // obtener valor
      v := session.Get("count")
      if v == nil {
         count = 0
      } else {
         count = v.(int)
         count++
      }
      // establecer
      session.Set("count", count)
      // guardar
      session.Save()
      c.JSON(200, gin.H{"count": count})
   })
   r.Run(":8000")
}

Generalmente no se recomienda almacenar Sesión a través de Cookie, se recomienda usar Redis, otros ejemplos por favor ve al repositorio oficial para entender.

JWT

Ventajas:

  • Basado en JSON, multi-lenguaje通用
  • Puede almacenar información no sensible
  • Ocupa muy poco, fácil de transmitir
  • El servidor no necesita almacenar, favorable para la expansión distribuida

Desventajas:

  • Problema de actualización de Token
  • Una vez emitido, no se puede controlar activamente

Desde la revolución del frontend, los programadores de frontend ya no son solo "escribir páginas", la tendencia de separación de frontend y backend es cada vez más fuerte, JWT es el más adecuado para el control de sesiones en sistemas de separación de frontend y backend y sistemas distribuidos, tiene grandes ventajas naturales. Considerando que JWT se ha separado completamente del contenido de Gin, y no hay soporte de middleware, porque JWT en sí no se limita a ningún framework o lenguaje, aquí no se dará una explicación detallada, puedes ir a otro documento: JWT

Gestión de registros

El middleware de registro predeterminado de Gin usa os.Stdout, solo tiene las funciones más básicas, después de todo Gin solo se enfoca en el servicio web, en la mayoría de los casos se debe usar un framework de registro más maduro, pero esto no está dentro del alcance de este capítulo, y la escalabilidad de Gin es muy alta, se puede integrar fácilmente con otros frameworks, aquí solo se discute su servicio de registro incorporado.

Color de consola

go
gin.DisableConsoleColor() // desactivar color de registro de consola

Excepto durante el desarrollo, la mayoría de las veces no se recomienda activar este elemento

Registro en archivo

go
func main() {
  e := gin.Default()
    // desactivar color de consola
  gin.DisableConsoleColor()
    // crear dos archivos de registro
  log1, _ := os.Create("info1.log")
  log2, _ := os.Create("info2.log")
    // registrar simultáneamente en dos archivos de registro
  gin.DefaultWriter = io.MultiWriter(log1, log2)
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

El registro incorporado de gin soporta escribir en múltiples archivos, pero el contenido es el mismo, no es muy conveniente de usar, y no escribirá registros de solicitud en los archivos.

go
func main() {
  router := gin.New()
  // El middleware LoggerWithFormatter escribirá registros en gin.DefaultWriter
  // por defecto gin.DefaultWriter = os.Stdout
  router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        //TODO escribir lógica del archivo correspondiente
        ......
    // output formato personalizado
    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")
}

A través de middleware personalizado, se puede lograr que los registros se escriban en archivos

Formato de registro de depuración de rutas

Aquí solo se modifica el registro de información de rutas de salida al iniciar

go
func main() {
   e := gin.Default()
   gin.SetMode(gin.DebugMode)
   gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
      if gin.IsDebugging() {
         log.Printf("ruta %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
      }
   }
   e.GET("/hello", Hello)
   log.Fatalln(e.Run(":8080"))
}

Salida

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

Conclusión: Gin es el framework web más fácil de aprender en el lenguaje Go, porque Gin realmente logra la minimización de responsabilidades, solo es responsable del servicio web, otras lógicas de autenticación, caché de datos, etc. se dejan a los desarrolladores para que las completen por sí mismos, en comparación con esos frameworks grandes y completos, Gin ligero y limpio es más adecuado y debería ser aprendido por los principiantes, porque Gin no obliga el uso de una cierta norma, cómo construir el proyecto, qué estructura adoptar todo necesita ser considerado por uno mismo, para los principiantes es más capaz de ejercitar la capacidad.

Golang editado por www.golangdev.cn