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.
go get -u github.com/gin-gonic/ginImportar
import "github.com/gin-gonic/gin"Início Rápido
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
GET localhost:8080/pingRetorno
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.
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
curl --location --request GET '127.0.0.1:8080/findUser/jack/001'username is jack
userid is 001Exemplo 2
curl --location --request GET '127.0.0.1:8080/downloadFile/img/fruit.png'filepath is /img/fruit.pngParâmetros de URL
Parâmetros de URL tradicionais, o 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)
}Exemplo 1
curl --location --request GET '127.0.0.1:8080/findUser?username=jack&userid=001'username is jack
userid is 001Exemplo 2
curl --location --request GET '127.0.0.1:8080/findUser'username is defaultUser
userid isParâ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.
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
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
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.
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
c.MustBindWith(obj, binding.JSON) //json
c.MustBindWith(obj, binding.XML) //xmlOs tipos de vinculação suportados pelo gin são as seguintes implementações:
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
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
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
curl --location --request POST '127.0.0.1:8080/loginWithForm' \
--form 'username="root"' \
--form 'password="root"'login successfully !Vinculação de Dados de URL
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:
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.
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
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' tagTIP
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
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
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
<!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:
// 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) boolPara 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.
// 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() *Contextfunc 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
curl --location --request GET 'http://localhost:8080/hello'Saída
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
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
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.
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 GMTNão acha que é simples demais? Que tal não usar o método do framework e escrever o processo por conta própria
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.
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.
func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroupAo criar um grupo, também podemos registrar handlers para a rota raiz do grupo, mas na maioria das vezes não fazemos isso.
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
func (engine *Engine) NoRoute(handlers ...HandlerFunc)Vamos pegar o exemplo anterior
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
func (engine *Engine) NoMethod(handlers ...HandlerFunc)Para registrar um handler, para chamar automaticamente quando ocorrer, pré-requisito é definir Engine.HandleMethodNotAllowed = true.
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 allowedConfiguração concluída
Redirecionamento
Redirecionamento no gin é muito simples, basta chamar o método 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")
}Teste
curl --location --request GET 'http://localhost:8080/'Saída
helloMiddleware
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
// 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.
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.
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().
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.
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().
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.
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
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
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.
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: 517600Um 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.
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
// 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) IRoutesTIP
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
|
|-- htmlfunc 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.
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.
Cookie
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
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/sessionsSuporta cookie, Redis, MongoDB, GORM, PostgreSQL
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
gin.DisableConsoleColor() // Desativa cor de log no consoleExceto durante desenvolvimento, na maioria das vezes não é recomendado ativar este item
Log Escrito em Arquivo
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.
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
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 3Conclusã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.
