Skip to content

Gin

Documentação oficial: Gin Web Framework (gin-gonic.com)

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

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

Introdução

Gin é um framework web escrito em Go (Golang). Possui uma API similar ao martini, com desempenho muito superior, graças ao httprouter, que é 40 vezes mais rápido. Se você precisa de desempenho e boa produtividade, certamente vai gostar do Gin. Gin em comparação com Iris e Beego tende a ser um framework mais leve, responsável apenas pela parte web, buscando desempenho máximo de roteamento. As funcionalidades podem não ser tão completas, mas é leve e fácil de expandir, o que também é uma vantagem. Portanto, entre todos os frameworks web, Gin é o mais fácil de aprender e usar.

Características

  • Rápido: Roteamento baseado em árvore Radix, pequena ocupação de memória. Sem reflexão. Desempenho de API previsível.
  • Suporte a middleware: Requisições HTTP recebidas podem ser processadas por uma série de middlewares e operações finais. Por exemplo: Logger, Authorization, GZIP, operações finais no banco de dados.
  • Tratamento de Crash: Gin pode capturar um panic que ocorre durante uma requisição HTTP e fazer recover. Assim, seu servidor estará sempre disponível.
  • Validação JSON: Gin pode analisar e validar o JSON da requisição, como verificar a presença de valores necessários.
  • Grupos de rotas: Melhor organização de rotas. Precisa de autorização, diferentes versões de API... Além disso, esses grupos podem ser aninhados sem limitações sem degradar o desempenho.
  • Gerenciamento de erros: Gin fornece uma maneira conveniente de coletar todos os erros que ocorrem durante requisições HTTP. Finalmente, middlewares podem gravá-los em arquivos de log, banco de dados e enviar pela rede.
  • Renderização integrada: Gin fornece APIs fáceis de usar para renderização JSON, XML e HTML.
  • Extensibilidade: Criar um novo middleware é muito simples

Instalação

Até o momento 2022/11/22, a versão mínima do Go suportada pelo gin é 1.16, recomenda-se usar go mod para gerenciar dependências do projeto.

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

Importar

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

Início Rápido

go
package main

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

func main() {
   engine := gin.Default() // criar engine gin
   engine.GET("/ping", func(context *gin.Context) {
      context.JSON(http.StatusOK, gin.H{
         "message": "pong",
      })
   })
   engine.Run() // iniciar servidor, padrão localhost:8080
}

Requisitar URL

http
GET localhost:8080/ping

Retorno

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

Documentação

Na verdade, a documentação oficial do Gin não tem muitos tutoriais, a maioria são apenas algumas introduções, uso básico e exemplos, mas sob a organização gin-gonic/, há um repositório gin-gonic/examples, que é um repositório de exemplos mantido pela comunidade. Está todo em inglês, e a frequência de atualização não é muito alta, mas o autor também está aprendendo o framework gin a partir daqui lentamente.

Repositório de exemplos: gin-gonic/examples: A repository to host examples and tutorials for Gin. (github.com)

TIP

Antes de começar, é recomendável ler sobre HttpRouter: HttpRouter

Análise de Parâmetros

A análise de parâmetros no gin suporta três métodos no total: parâmetros de rota, parâmetros de URL, parâmetros de formulário. Abaixo estão explicados um por um com exemplos de código, simples e fáceis de entender.

Parâmetros de Rota

Parâmetros de rota são na verdade uma encapsulação da funcionalidade de análise de parâmetros do HttpRouter, o método de uso é basicamente o mesmo do 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"))
}

// Exemplo de parâmetro nomeado
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)
}

// Exemplo de parâmetro de caminho
func UserPage(c *gin.Context) {
   filepath := c.Param("filepath")
   c.String(http.StatusOK, "filepath is  %s", filepath)
}

Exemplo 1

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

Exemplo 2

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

Parâmetros de URL

Parâmetros de URL tradicionais, o 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)
}

Exemplo 1

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

Exemplo 2

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

Parâmetros de Formulário

O tipo de conteúdo do formulário geralmente é 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"])
}

Exemplo 1: Usando 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]

O método PostForm analisa por padrão formulários do tipo application/x-www-form-urlencoded e multipart/form-data.

Exemplo 2: Usando 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]

Análise de Dados

Na maioria dos casos, usamos structs para carregar dados em vez de analisar parâmetros diretamente. No gin, os métodos principais para vinculação de dados são Bind() e ShouldBind(), a diferença entre os dois é que o primeiro chama internamente ShouldBind(), e ao retornar err, faz uma resposta 400 diretamente, enquanto o segundo não. Se quiser mais flexibilidade no tratamento de erros, é recomendado usar o segundo. Essas duas funções inferem automaticamente com base no content-type da requisição qual método de análise usar.

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

Se quiser escolher manualmente, pode usar BindWith() e ShouldBindWith(), por exemplo

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

Os tipos de vinculação suportados pelo gin são as seguintes implementações:

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

Exemplo

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 para让 gin inferir 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)
}

Vinculação de Dados 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 !

Vinculação de Dados de Formulário

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

Vinculação de Dados de URL

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

Aqui ocorre um erro, porque o content-type de saída é uma string vazia, não é possível inferir como analisar os dados. Portanto, ao usar parâmetros de URL, devemos especificar manualmente o método de análise, por exemplo:

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

Múltiplas Vinculações

Geralmente os métodos vinculam dados chamando c.Request.Body, mas não é possível chamar este método múltiplas vezes, por exemplo c.ShouldBind, não é reutilizável. Se quiser vincular múltiplas vezes, pode usar c.ShouldBindBodyWith.

go
func SomeHandler(c *gin.Context) {
  objA := formA{}
  objB := formB{}
  // Lê c.Request.Body e armazena o resultado no contexto.
  if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil {
    c.String(http.StatusOK, `the body should be formA`)
  // Neste ponto, reutiliza o body armazenado no contexto.
  }
  if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil {
    c.String(http.StatusOK, `the body should be formB JSON`)
  // Pode aceitar outros formatos
  }
  if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil {
    c.String(http.StatusOK, `the body should be formB XML`)
  }
}

TIP

c.ShouldBindBodyWith armazena o body no contexto antes de vincular. Isso tem um leve impacto no desempenho, se puder ser concluído com uma única chamada, não use este método. Apenas alguns formatos precisam desta funcionalidade, como JSON, XML, MsgPack, ProtoBuf. Para outros formatos, como Query, Form, FormPost, FormMultipart pode chamar c.ShouldBind() múltiplas vezes sem qualquer perda de desempenho.

Validação de Dados

A ferramenta de validação integrada do gin é na verdade github.com/go-playground/validator/v10, o método de uso é quase idêntico, Validator

Exemplo Simples

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

Teste

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

}'

Saída

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

TIP

Um ponto importante a notar é que a tag de validação do validator no gin é binding, enquanto ao usar validator separadamente a tag de validação é validator

Resposta de Dados

A resposta de dados é o último passo no tratamento de interfaces, o backend retorna todos os dados processados ao chamador através do protocolo HTTP, gin fornece suporte rico para resposta de dados, uso simples e fácil de aprender.

Exemplo Simples

go
func Hello(c *gin.Context) {
    // Retorna dados em formato de string pura, http.StatusOK representa código de status 200, dados são "Hello world !"
  c.String(http.StatusOK, "Hello world !")
}

Renderização HTML

TIP

Ao carregar arquivos, o caminho raiz padrão é o caminho do projeto, ou seja, o caminho onde o arquivo go.mod está localizado, index.html nos exemplos abaixo está localizado em index.html no caminho raiz, mas geralmente esses arquivos de modelo não são colocados no caminho raiz, mas em pastas de recursos estáticos

go
func main() {
   e := gin.Default()
    // Carrega arquivo HTML, também pode 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{})
}

Teste

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

Retorno

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>

Resposta Rápida

Mencionamos frequentemente o método context.String() para resposta de dados, este é o método de resposta mais原始, retorna diretamente uma string, gin também possui muitos métodos de resposta rápida embutidos, por exemplo:

go
// Usa Render para escrever cabeçalho de resposta e fazer renderização de dados
func (c *Context) Render(code int, r render.Render)

// Renderiza um template HTML, name é caminho html, obj é conteúdo
func (c *Context) HTML(code int, name string, obj any)

// Renderização de dados com string JSON formatada com indentação, geralmente não recomendado usar este método, pois causa mais consumo de transmissão.
func (c *Context) IndentedJSON(code int, obj any)

// JSON seguro, pode prevenir sequestro JSON, detalhes: https://www.cnblogs.com/xusion/articles/3107788.html
func (c *Context) SecureJSON(code int, obj any)

// Renderização via JSONP
func (c *Context) JSONP(code int, obj any)

// Renderização via JSON
func (c *Context) JSON(code int, obj any)

// Renderização via JSON, converte código unicode para ASCII
func (c *Context) AsciiJSON(code int, obj any)

// Renderização via JSON, não faz escape de caracteres especiais HTML
func (c *Context) PureJSON(code int, obj any)

// Renderização via XML
func (c *Context) XML(code int, obj any)

// Renderização via YML
func (c *Context) YAML(code int, obj any)

// Renderização via TOML
func (c *Context) TOML(code int, obj interface{})

// Renderização via ProtoBuf
func (c *Context) ProtoBuf(code int, obj any)

// Renderização via String
func (c *Context) String(code int, format string, values ...any)

// Redireciona para local específico
func (c *Context) Redirect(code int, location string)

// Escreve data no fluxo de resposta
func (c *Context) Data(code int, contentType string, data []byte)

// Lê fluxo através de reader e escreve no fluxo de resposta
func (c *Context) DataFromReader(code int, contentLength int64, contentType string, reader io.Reader, extraHeaders map[string]string)

// Escreve arquivo no fluxo de resposta de forma eficiente
func (c *Context) File(filepath string)

// Escreve fluxo de arquivo do fs no fluxo de resposta de forma eficiente
func (c *Context) FileFromFS(filepath string, fs http.FileSystem)

// Escreve fluxo de arquivo do fs no fluxo de resposta de forma eficiente, e no cliente será baixado com nome de arquivo especificado
func (c *Context) FileAttachment(filepath, filename string)

// Escreve fluxo de push do servidor no fluxo de resposta
func (c *Context) SSEvent(name string, message any)

// Envia uma resposta de fluxo e retorna um booleano, para determinar se o cliente desconectou no meio do fluxo
func (c *Context) Stream(step func(w io.Writer) bool) bool

Para a maioria das aplicações, o mais usado ainda é context.JSON, outros são relativamente menos usados, aqui não serão demonstrados com exemplos, pois são todos simples e fáceis de entender, basicamente chamadas diretas.

Processamento Assíncrono

No gin, processamento assíncrono precisa ser combinado com goroutine, uso é muito simples.

go
// copy retorna uma cópia do Context atual para uso seguro fora do escopo do Context atual, pode ser usado para passar para uma 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() {
    // Sub-rotina deve usar cópia do Context, não deve usar Context original
    log.Println("Função de processamento assíncrono: ", ctx.HandlerNames())
  }()
  log.Println("Função de processamento de interface: ", c.HandlerNames())
  c.String(http.StatusOK, "hello")
}

Teste

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

Saída

go
2022/12/21 13:33:47 Função de processamento assíncrono:  []
2022/12/21 13:33:47 Função de processamento de interface:  [github.com/gin-gonic/gin.LoggerWithConfig.func1 github.com/gin-gonic/gin.CustomRecoveryWithWriter.func1 main.Hello]
[GIN] 2022/12/21 - 13:33:47 | 200 |     11.1927ms |             ::1 | GET      "/hello"

Pode-se ver que as saídas são diferentes, a cópia ao replicar, por questões de segurança, removeu muitos valores dos elementos.

Transferência de Arquivos

Transferência de arquivos é uma funcionalidade indispensável em aplicações web, gin também encapsula isso de forma muito simples, mas essencialmente o processo é quase o mesmo que usar net/http nativo. O processo é ler o fluxo de arquivo do corpo da requisição e depois salvar localmente.

Upload de Arquivo Único

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

func uploadFile(ctx *gin.Context) {
  // Obtém arquivo
  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
  }
  // Retorna resultado
  ctx.String(http.StatusOK, "upload %s size:%d byte successfully!", file.Filename, file.Size)
}

Teste

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

Resultado

upload a.jpg size:1424 byte successfully!

TIP

Geralmente, o Method para upload de arquivos sempre especifica POST, algumas empresas podem preferir usar PUT, o primeiro é uma requisição HTTP simples, o segundo é uma requisição HTTP complexa, diferenças específicas não serão detalhadas, se usar o segundo, especialmente em projetos com separação frontend/backend, é necessário fazer tratamento CORS correspondente, e a configuração padrão do Gin não suporta CORS Configuração CORS.

Upload de Múltiplos Arquivos

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

func uploadFiles(ctx *gin.Context) {
  // Obtém formulário multipart analisado pelo gin
  form, _ := ctx.MultipartForm()
  // Obtém lista de arquivos correspondente pela chave
  files := form.File["files"]
  // Itera lista de arquivos, salva localmente
  for _, file := range files {
    err := ctx.SaveUploadedFile(file, "./"+file.Filename)
    if err != nil {
      ctx.String(http.StatusBadRequest, "upload failed")
      return
    }
  }
  // Retorna resultado
  ctx.String(http.StatusOK, "upload %d files successfully!", len(files))
}

Teste

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

Saída

upload 3 files successfully!

Download de Arquivos

Sobre a parte de download de arquivos, Gin encapsula ainda mais a API da biblioteca padrão original, tornando o download de arquivos extremamente simples.

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) {
    // Obtém nome do arquivo
  filename := ctx.Param("filename")
    // Retorna arquivo correspondente
  ctx.FileAttachment(filename, filename)
}

Teste

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

Não acha que é simples demais? Que tal não usar o método do framework e escrever o processo por conta própria

go
func download(ctx *gin.Context) {
   // Obtém parâmetro
   filename := ctx.Param("filename")

   // Objeto de resposta de requisição e objeto de requisição
   response, request := ctx.Writer, ctx.Request
   // Escreve cabeçalho de resposta
   // response.Header().Set("Content-Type", "application/octet-stream")  transmite arquivo como fluxo binário
   response.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename)) // faz escape de segurança no nome do arquivo
   response.Header().Set("Content-Transfer-Encoding", "binary")                                            // codificação de transmissão
   http.ServeFile(response, request, filename)
}

Na verdade net/http já é suficientemente bem encapsulado

TIP

Pode usar Engine.MaxMultipartMemory para definir memória máxima para transferência de arquivos, padrão é 32 << 20 // 32 MB

Gerenciamento de Rotas

Gerenciamento de rotas é uma parte muito importante em um sistema, é necessário garantir que cada requisição seja mapeada corretamente para a função correspondente.

Grupos de Rotas

Criar um grupo de rotas é classificar interfaces, interfaces de diferentes categorias correspondem a diferentes funções, também é mais fácil de gerenciar.

go
func Hello(c *gin.Context) {

}

func Login(c *gin.Context) {

}

func Update(c *gin.Context) {

}

func Delete(c *gin.Context) {

}

Suponha que temos quatro interfaces acima, temporariamente não importa sua implementação interna, Hello, Login são um grupo, Update, Delete são um grupo.

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

Ao criar um grupo, também podemos registrar handlers para a rota raiz do grupo, mas na maioria das vezes não fazemos isso.

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

Dividimos em dois grupos v1, v2, as chaves {} são apenas para padronização, indicam que handlers registrados dentro das chaves pertencem ao mesmo grupo de rotas, não têm função funcional. Da mesma forma, gin suporta grupos aninhados, método é o mesmo do exemplo acima, aqui não será demonstrado.

Rota 404

A estrutura Engine no gin fornece um método NoRoute, para definir como lidar quando a URL acessada não existe, desenvolvedores podem escrever lógica neste método, para chamar automaticamente quando a rota não for encontrada, por padrão retorna código de status 404

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

Vamos pegar o exemplo 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)
   }
   // Registra handler
   e.NoRoute(func(context *gin.Context) { // aqui é apenas demonstração, não retorne código HTML diretamente em ambiente de produção
      context.String(http.StatusNotFound, "<h1>404 Page Not Found</h1>")
   })
   log.Fatalln(e.Run(":8080"))
}

Envie uma requisição aleatória

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

Rota 405

No código de status Http, 405 representa que o tipo de método da requisição atual não é permitido, gin fornece o seguinte método

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

Para registrar um handler, para chamar automaticamente quando ocorrer, pré-requisito é definir Engine.HandleMethodNotAllowed = true.

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

Após configurar, o header padrão do gin não suporta requisições OPTION, teste

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

Configuração concluída

Redirecionamento

Redirecionamento no gin é muito simples, basta chamar o 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")
}

Teste

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

Saída

hello

Middleware

Gin é muito leve e flexível, com extensibilidade muito alta, e suporte muito amigável para middlewares. No Gin, todas as requisições de interface passam por middlewares, através de middlewares, desenvolvedores podem implementar muitas funcionalidades e lógicas customizadas. Embora o gin tenha poucas funcionalidades embutidas, os middlewares de extensão desenvolvidos pela comunidade de terceiros são muito ricos.

Middleware é essencialmente ainda um handler de interface

go
// HandlerFunc define o handler usado pelo middleware gin como valor de retorno.
type HandlerFunc func(*Context)

De certa forma, cada handler correspondente a uma requisição também é um middleware, mas é um middleware local de escopo muito pequeno.

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

Verificando o código fonte do gin, na função Default, a Engine padrão retornada usa dois middlewares padrão Logger(), Recovery(), se não quiser usar os middlewares padrão, pode usar gin.New() como alternativa.

Middleware Global

Middleware global tem escopo global, todas as requisições de todo o sistema passam por este middleware.

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

Primeiro cria uma função closure para criar o middleware, depois registra o middleware global através de Engine.Use().

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

Teste

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

Saída

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

Middleware Local

Middleware local tem escopo local, requisições locais do sistema passam por este middleware. Middleware local pode ser registrado em rotas únicas, mas na maioria das vezes é registrado em grupos de rotas.

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

Teste

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

Saída

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

Princípio do Middleware

O uso e customização de middlewares no Gin é muito fácil, seu princípio interno também é relativamente simples, para aprendizado posterior, é necessário entender simplesmente o princípio interno. Middlewares no Gin na verdade usam o padrão Chain of Responsibility, Context mantém um HandlersChain, essencialmente um []HandlerFunc, e um index, cujo tipo de dado é int8. No método Engine.handlerHTTPRequest(c *Context), há um trecho de código que indica o processo de chamada: após o gin encontrar a rota correspondente na árvore de rotas, chama o método Next().

go
if value.handlers != nil {
   // Atribui cadeia de chamada ao Context
   c.handlers = value.handlers
   c.fullPath = value.fullPath
   // Chama middleware
   c.Next()
   c.writermem.WriteHeaderNow()
   return
}

A chamada de Next() é a chave, Next() itera sobre os HandlerFunc nos handlers da rota e executa, aqui pode-se ver que a função do index é registrar a posição de chamada do middleware. Dentre eles, a função de interface registrada para a rota correspondente também está em handlers, é por isso que foi dito anteriormente que interface também é um middleware.

go
func (c *Context) Next() {
   // +1 ao entrar é para evitar loop recursivo infinito, valor padrão é -1
   c.index++
   for c.index < int8(len(c.handlers)) {
      // Executa HandlerFunc
      c.handlers[c.index](c)
      // Após execução, index+1
      c.index++
   }
}

Modifica a lógica de Hello(), para verificar se é realmente assim

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

Resultado da saída é

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

Pode-se ver que a ordem da cadeia de chamada de middlewares é: Logger -> Recovery -> GlobalMiddleware -> LocalMiddleWare -> Hello, o último elemento da cadeia de chamada é a função de interface verdadeira a ser executada, os anteriores são middlewares.

TIP

Ao registrar rotas locais, há a seguinte asserção

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

Dentre eles abortIndex int8 = math.MaxInt8 >> 1 valor é 63, ou seja, ao usar o sistema, o número de registros de rotas não deve exceder 63.

Middleware de Temporizador

Após conhecer o princípio do middleware acima, é possível escrever um middleware simples de estatística de tempo de requisição.

go
func TimeMiddleware() gin.HandlerFunc {
   return func(context *gin.Context) {
      // Registra tempo de início
      start := time.Now()
      // Executa cadeia de chamada subsequente
      context.Next()
      // Calcula intervalo de tempo
      duration := time.Since(start)
      // Saída em nanossegundos, para observar resultados
      fmt.Println("Tempo de requisição: ", duration.Nanoseconds())
   }
}

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

Teste

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

Saída

Tempo de requisição:  517600

Um middleware de temporizador simples já foi编写完毕, posteriormente pode凭自己的摸索编写一些功能更实用的中间件。

Configuração de Serviço

Apenas usar a configuração padrão está longe de ser suficiente, na maioria dos casos é necessário modificar muitas configurações de serviço para atender aos requisitos.

Configuração Http

Pode criar Server através de net/http para configurar, Gin também suporta usar Gin como 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())
}

Configuração de Recursos Estáticos

Recursos estáticos no passado eram basicamente uma parte indispensável do servidor, embora atualmente a proporção de uso esteja gradualmente diminuindo, ainda há um grande número de sistemas que ainda usam arquitetura monolítica.

Gin fornece três métodos para carregar recursos estáticos

go
// Carrega uma pasta estática específica
func (group *RouterGroup) Static(relativePath, root string) IRoutes

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

// Carrega um arquivo estático específico
func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes

TIP

relativePath é o caminho relativo mapeado para URL da web, root é o caminho real do arquivo no projeto

Suponha que a estrutura de diretórios do projeto seja a seguinte

root
|
|-- static
|  |
|  |-- a.jpg
|  |
|  |-- favicon.ico
|
|-- view
  |
  |-- html
go
func main() {
   router := gin.Default()
   // Carrega diretório de arquivos estáticos
   router.Static("/static", "./static")
   // Carrega diretório de arquivos estáticos
   router.StaticFS("/view", http.Dir("view"))
   // Carrega arquivo estático
   router.StaticFile("/favicon", "./static/favicon.ico")

   router.Run(":8080")
}

Configuração de CORS

Gin não faz nenhum tratamento para configuração de CORS, é necessário escrever middleware por conta própria para implementar requisitos correspondentes, na verdade a dificuldade não é grande, pessoas familiarizadas com protocolo HTTP geralmente conseguem escrever, a lógica é basicamente a mesma.

go
func CorsMiddle() gin.HandlerFunc {
   return func(c *gin.Context) {
      method := c.Request.Method
      origin := c.Request.Header.Get("Origin")
      if origin != "" {
         // Em ambiente de produção, servidores geralmente não preenchem *, devem preencher domínio específico
         c.Header("Access-Control-Allow-Origin", origin)
         // HTTP METHOD permitidos
         c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, UPDATE")
         // Cabeçalhos de requisição permitidos
         c.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
         // Cabeçalhos de resposta permitidos para cliente acessar
         c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Cache-Control, Content-Language, Content-Type")
         // Se precisa carregar informações de autenticação Credentials pode ser cookies, authorization headers ou certificados de cliente TLS
         // Ao definir como true, Access-Control-Allow-Origin não pode ser *
         c.Header("Access-Control-Allow-Credentials", "true")
      }
      // Libera requisição OPTION, mas não executa métodos subsequentes
      if method == "OPTIONS" {
         c.AbortWithStatus(http.StatusNoContent)
      }
      // Libera
      c.Next()
   }
}

Registra o middleware como middleware global即可

Controle de Sessão

Na era atual, os três tipos populares de controle de sessão web são cookie, session, JWT.

Informações no cookie são armazenadas em formato de chave-valor no navegador, e os dados podem ser vistos diretamente no navegador

Vantagens:

  • Estrutura simples
  • Dados persistentes

Desvantagens:

  • Tamanho limitado
  • Armazenamento em texto claro
  • Fácil de sofrer ataques CSRF
go
import (
    "fmt"

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

func main() {

    router := gin.Default()

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

         // Obtém cookie correspondente
        cookie, err := c.Cookie("gin_cookie")

        if err != nil {
            cookie = "NotSet"
            // Define cookie 参数:key, val, tempo de existência, diretório, domínio, se permite outros acessarem cookie via js, apenas http
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

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

    router.Run()
}

Cookie puro era mais usado há cinco ou seis anos, mas o autor raramente usa cookie puro para controle de sessão, fazer isso realmente não é muito seguro.

Session

Session é armazenada no servidor, depois envia um cookie armazenado no navegador, cookie armazena session_id, depois cada requisição ao servidor pode obter informações da session correspondente através do session_id

Vantagens:

  • Armazenado no lado do servidor, aumenta segurança, facilita gerenciamento

Desvantagens:

  • Armazenado no lado do servidor, aumenta custo do servidor, reduz desempenho
  • Baseado em identificação por cookie, inseguro
  • Informações de autenticação não são sincronizadas em situação distribuída

Session e Cookie são inseparáveis, sempre que se usa Session, por padrão é necessário usar Cookie. Gin por padrão não suporta Session, porque Cookie é conteúdo do protocolo Http, mas Session não, porém há suporte de middleware de terceiros, basta instalar dependência, endereço do repositório: gin-contrib/sessions: Gin middleware for session management (github.com)

go get github.com/gin-contrib/sessions

Suporta cookie, Redis, MongoDB, GORM, PostgreSQL

go
func main() {
   r := gin.Default()
   // Cria engine de armazenamento baseada em Cookie
   store := cookie.NewStore([]byte("secret"))
   // Define middleware de Session, mysession é nome da session, também é nome do cookie
   r.Use(sessions.Sessions("mysession", store))
   r.GET("/incr", func(c *gin.Context) {
      // Inicializa session
      session := sessions.Default(c)
      var count int
      // Obtém valor
      v := session.Get("count")
      if v == nil {
         count = 0
      } else {
         count = v.(int)
         count++
      }
      // Define
      session.Set("count", count)
      // Salva
      session.Save()
      c.JSON(200, gin.H{"count": count})
   })
   r.Run(":8000")
}

Geralmente não é recomendado armazenar Session através de Cookie, recomenda-se usar Redis, outros exemplos por favor consulte o repositório oficial.

JWT

Vantagens:

  • Baseado em JSON, genérico para múltiplas linguagens
  • Pode armazenar informações não sensíveis
  • Ocupa pouco espaço, facilita transmissão
  • Servidor não precisa armazenar, favorece expansão distribuída

Desvantagens:

  • Problema de atualização de Token
  • Uma vez emitido, não pode ser controlado ativamente

Desde a revolução frontend, programadores frontend não são mais apenas "escrever páginas", a tendência de separação frontend/backend é cada vez mais forte, JWT é o mais adequado para controle de sessão em separação frontend/backend e sistemas distribuídos, tem grandes vantagens naturais. Considerando que JWT já saiu completamente do conteúdo do Gin, e não há suporte de middleware, porque JWT em si não se limita a nenhum framework ou linguagem, aqui não será feita uma explicação detalhada, pode ir para outro documento: JWT

Gerenciamento de Log

O middleware de log padrão usado pelo Gin usa os.Stdout, possui apenas funcionalidades básicas, afinal Gin foca apenas em serviço web, na maioria dos casos deve-se usar framework de log mais maduro, mas isso não está no escopo de discussão deste capítulo, e a extensibilidade do Gin é muito alta, pode integrar facilmente outros frameworks, aqui discutimos apenas seu serviço de log embutido.

Cor do Console

go
gin.DisableConsoleColor() // Desativa cor de log no console

Exceto durante desenvolvimento, na maioria das vezes não é recomendado ativar este item

Log Escrito em Arquivo

go
func main() {
  e := gin.Default()
    // Desativa cor do console
  gin.DisableConsoleColor()
    // Cria dois arquivos de log
  log1, _ := os.Create("info1.log")
  log2, _ := os.Create("info2.log")
    // Registra simultaneamente em dois arquivos de log
  gin.DefaultWriter = io.MultiWriter(log1, log2)
  e.GET("/hello", Hello)
  log.Fatalln(e.Run(":8080"))
}

O log embutido do gin suporta escrita em múltiplos arquivos, mas o conteúdo é o mesmo, uso não é muito conveniente, e não grava logs de requisição nos arquivos.

go
func main() {
  router := gin.New()
  // Middleware LoggerWithFormatter grava logs em gin.DefaultWriter
  // Por padrão gin.DefaultWriter = os.Stdout
  router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
        //TODO lógica de escrita no arquivo correspondente
        ......
    // Saída de formato customizado
    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")
}

Através de middleware customizado, é possível implementar logs escritos em arquivos

Formato de Log de Depuração de Rotas

Aqui modifica-se apenas o log de saída de informações de rotas ao 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("Rota %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers)
      }
   }
   e.GET("/hello", Hello)
   log.Fatalln(e.Run(":8080"))
}

Saída

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

Conclusão: Gin é o framework web mais fácil de aprender na linguagem Go, porque Gin realmente alcançou minimização de responsabilidades, é apenas responsável por serviço web, outras lógicas de autenticação, cache de dados etc. são deixadas para os desenvolvedores completarem por conta própria, comparado com frameworks grandes e completos, Gin leve e conciso é mais adequado e deveria ser aprendido por iniciantes, porque Gin não força o uso de alguma norma específica, como construir o projeto, que estrutura adotar, tudo precisa ser ponderado por conta própria, para iniciantes é mais capaz de exercitar habilidades.

Golang por www.golangdev.cn edit