Capítulo 02: Roteamento Avançado, Parâmetros de URL e Grupos de Rotas
Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["/api/v1 (Group)"] --> B["/api/v1/users (Sub-Group)"]
A --> C["/api/v1/orders (Sub-Group)"]
B --> D[":id (Parâmetro Named)"]
C --> E["*filepath (Wildcard)"]
B --> F["Middleware do Grupo"]
C --> F
subgraph QUERY["Query String"]
G["c.Query / c.DefaultQuery"]
end
D --> QUERY
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Fundamentos Técnicos de Roteamento Avançado, Parâmetros de URL e Grupos de Rotas
O roteador do Gin distingue três tipos de segmento em uma rota, e essa distinção importa porque a Radix Tree resolve conflitos entre eles em tempo de registro (não de requisição): segmentos estáticos (/users/profile), parâmetros nomeados (/users/:id, capturados com c.Param("id")) e wildcards (/files/*filepath, que casam com tudo que resta do caminho, inclusive barras). Uma regra prática que costuma surpreender iniciantes: o Gin não permite registrar /users/:id e /users/new como irmãos ambíguos no mesmo nível se isso gerar conflito de prioridade — ele resolve isso em tempo de boot, entrando em pânico (panic) se detectar uma rota irreconciliável, o que é uma proteção deliberada contra comportamento indefinido em produção.
Parâmetros de query string são um mecanismo totalmente separado dos parâmetros de rota: c.Query("page") devolve string vazia se a chave não existir, enquanto c.DefaultQuery("page", "1") devolve um valor de fallback. Já c.GetQuery("page") devolve (string, bool), útil quando é preciso diferenciar “parâmetro ausente” de “parâmetro enviado vazio”.
Grupos de rotas (router.Group("/api/v1")) não são apenas açúcar sintático para prefixos — eles retornam um *gin.RouterGroup que herda e acumula a cadeia de middlewares do grupo pai. Isso permite compor politicas em camadas: um middleware de rate limiting no grupo raiz, autenticação no sub-grupo /admin, e uma verificação de papel (role) ainda mais específica no sub-grupo /admin/faturamento, sem repetir código. Grupos também são a base natural para versionamento de API (/api/v1 convivendo com /api/v2 no mesmo binário durante uma migração).
Um detalhe operacional relevante: por padrão o Gin também trata redirecionamento de barra final (RedirectTrailingSlash) e case-insensitive routing (RedirectFixedPath) — uma requisição para /users/ pode ser redirecionada automaticamente para /users (ou vice-versa) com 301/307. Em APIs REST estritas isso deve ser desabilitado explicitamente via gin.Engine{RedirectTrailingSlash: false} para evitar comportamento surpresa em clientes que não seguem redirects.
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func AuthAdminMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if c.GetHeader("X-Admin-Token") == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"erro": "token de admin ausente"})
return
}
c.Next()
}
}
func SetupRoutes(r *gin.Engine) {
v1 := r.Group("/api/v1")
{
v1.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
pagina := c.DefaultQuery("page", "1")
c.JSON(http.StatusOK, gin.H{"id": id, "pagina": pagina})
})
admin := v1.Group("/admin", AuthAdminMiddleware())
{
admin.GET("/relatorios/*arquivo", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"arquivo": c.Param("arquivo")})
})
}
}
}
💻 2. Código de Demonstração Corporativo
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
// RateLimitPlaceholder simula um middleware global aplicado a toda a API.
func RateLimitPlaceholder() gin.HandlerFunc {
return func(c *gin.Context) { c.Next() }
}
func RequireRole(papel string) gin.HandlerFunc {
return func(c *gin.Context) {
papelUsuario, existe := c.Get("papel")
if !existe || papelUsuario != papel {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"erro": "acesso negado"})
return
}
c.Next()
}
}
func main() {
engine := gin.New()
engine.Use(gin.Recovery(), RateLimitPlaceholder())
api := engine.Group("/api")
v1 := api.Group("/v1")
{
v1.GET("/produtos/:id", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"id": c.Param("id"),
"limite": c.DefaultQuery("limit", "20"),
})
})
financeiro := v1.Group("/financeiro", RequireRole("ADMIN"))
{
financeiro.GET("/relatorios/*arquivo", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"caminho_solicitado": c.Param("arquivo")})
})
}
}
engine.Run(":8080")
}