Roteamento e a ServeMux moderna
Esta aula explora o roteamento moderno do pacote net/http do Go a partir da versão 1.22, incluindo padrões de rota, métodos HTTP, path parameters e uma comparação com frameworks populares. Você aprenderá a criar servidores web mais expressivos e eficientes usando apenas a biblioteca padrão.
O pacote net/http do Go sempre foi a base para construir servidores web, mas por muitos anos seu roteamento era limitado: basicamente, você registrava um caminho exato (ou um prefixo com /) e um handler. Isso forçava muitos desenvolvedores a recorrer a frameworks como Gin ou Echo para ter rotas com parâmetros e métodos HTTP. A partir do Go 1.22, o roteamento padrão recebeu uma grande melhoria, tornando-o muito mais poderoso e próximo do que frameworks oferecem, sem a necessidade de dependências externas.
Nesta aula, vamos mergulhar nas novas capacidades do http.ServeMux: padrões de rota com curingas, restrição por método HTTP, captura de path parameters e uma análise comparativa com frameworks. Ao final, você será capaz de construir APIs RESTful completas usando apenas a biblioteca padrão, com código limpo e performático.
Padrões de rota (Go 1.22+)
Antes do Go 1.22, o http.ServeMux tratava padrões de duas formas: caminhos exatos (como "/hello") e subárvores (como "/static/"). Não havia suporte a curingas ou parâmetros. A partir da versão 1.22, o pacote net/http introduziu uma sintaxe de padrões mais rica, inspirada em frameworks como Gin e Echo.
A nova sintaxe permite usar {param} para capturar partes dinâmicas da URL, e ... para corresponder a qualquer sequência de segmentos. Além disso, você pode especificar o método HTTP no próprio padrão, como "GET /api/users". Isso torna o roteamento mais declarativo e reduz a necessidade de verificação manual dentro dos handlers.
Vamos ver um exemplo básico:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
// Rota exata
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Home")
})
// Rota com curinga {name}
mux.HandleFunc("/users/{name}", func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
fmt.Fprintf(w, "User: %s", name)
})
http.ListenAndServe(":8080", mux)
}No código acima, a rota /users/{name} captura o valor após /users/ e o disponibiliza via r.PathValue("name"). Isso elimina a necessidade de manipular manualmente r.URL.Path ou usar bibliotecas externas para extrair parâmetros.
Outra novidade é o curinga ..., que corresponde a qualquer número de segmentos. Por exemplo, /files/{path...} captura todo o restante do caminho. Isso é útil para servir arquivos estáticos ou montar sub-rotas.
mux.HandleFunc("/files/{path...}", func(w http.ResponseWriter, r *http.Request) {
path := r.PathValue("path")
fmt.Fprintf(w, "Requested file: %s", path)
})Com esses padrões, você pode construir rotas complexas de forma legível e direta, sem sacrificar a simplicidade da biblioteca padrão.
Métodos
No Go 1.22, é possível restringir um handler a um método HTTP específico (GET, POST, PUT, DELETE, etc.) diretamente no padrão da rota. Isso melhora a organização do código e evita que você precise verificar r.Method manualmente dentro de cada handler.
A sintaxe é simples: basta prefixar o padrão com o método desejado, seguido de um espaço. Por exemplo, "GET /api/users" só corresponderá a requisições GET. Se o método não corresponder, o ServeMux retornará automaticamente o status 405 (Method Not Allowed).
Veja um exemplo prático:
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)No código acima, cada rota está associada a um método específico. Isso torna o código mais organizado e elimina a necessidade de um grande switch dentro de um handler único. Além disso, a correspondência de padrões é mais precisa: se você registrar GET /api/users e POST /api/users, o roteador saberá exatamente qual handler usar com base no método.
É importante notar que, se você não especificar um método, o padrão corresponderá a todos os métodos HTTP. Isso é útil para rotas que devem aceitar qualquer método, como uma página de apresentação.
mux.HandleFunc("/health", healthHandler) // qualquer métodoCom essa funcionalidade, você pode implementar APIs RESTful completas usando apenas a biblioteca padrão, sem a necessidade de um framework para gerenciar métodos e rotas.
Path parameters
Path parameters (ou parâmetros de caminho) são valores dinâmicos embutidos na URL, como /users/123 onde 123 é um parâmetro. No Go 1.22, você pode capturá-los facilmente usando a sintaxe {nome} no padrão da rota e recuperá-los com o método r.PathValue("nome").
Vamos explorar um exemplo completo de uma API de usuários:
package main
import (
"fmt"
"net/http"
"strconv"
)
func main() {
mux := http.NewServeMux()
// Rota para obter um usuário específico
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid user ID", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "User ID: %d", id)
})
// Rota para atualizar um usuário
mux.HandleFunc("PUT /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "Updating user with ID: %s", id)
})
http.ListenAndServe(":8080", mux)
}No exemplo, a rota GET /users/{id} captura o valor de id como string. Você pode convertê-lo para o tipo desejado (como int) usando funções como strconv.Atoi. Isso é particularmente útil para validar e processar a entrada.
Além de parâmetros simples, você pode combinar múltiplos parâmetros em uma rota, como GET /users/{id}/posts/{postId}. O ServeMux os tratará de forma independente.
mux.HandleFunc("GET /users/{id}/posts/{postId}", func(w http.ResponseWriter, r *http.Request) {
userID := r.PathValue("id")
postID := r.PathValue("postId")
fmt.Fprintf(w, "User %s, Post %s", userID, postID)
})Uma característica importante é que os parâmetros são validados pelo próprio roteador: se uma rota exige um parâmetro e ele não está presente na URL, a requisição não corresponderá e o roteador tentará outras rotas. Isso evita erros de nil e simplifica a lógica do handler.
Outra funcionalidade é o curinga {path...}, que captura o restante do caminho como um único parâmetro (incluindo barras). Isso é útil para servir arquivos estáticos ou criar sub-rotas dinâmicas.
mux.HandleFunc("/files/{path...}", func(w http.ResponseWriter, r *http.Request) {
filePath := r.PathValue("path")
// Use filePath para localizar o arquivo
http.ServeFile(w, r, filepath.Join("./public", filePath))
})Com esses recursos, você pode criar URLs limpas e semânticas, melhorando a experiência do usuário e a manutenibilidade do código.
Comparação com frameworks
Por muitos anos, frameworks como Gin, Echo e Fiber foram a escolha padrão para desenvolvimento web em Go, principalmente por causa de seu roteamento avançado. Com a evolução do net/http, a biblioteca padrão agora oferece muitos desses recursos, mas ainda há diferenças importantes a considerar.
Vantagens da biblioteca padrão:
- Zero dependências: Você não precisa adicionar pacotes externos, o que reduz a superfície de ataque e simplifica o gerenciamento de dependências.
- Menor curva de aprendizado: Se você já conhece o
net/http, a transição para o novo roteamento é suave. - Performance: O
ServeMuxé altamente otimizado e, em muitos casos, comparável ou até mais rápido que frameworks. - Simplicidade: O código resultante é mais direto e fácil de entender, sem camadas de abstração.
Limitações da biblioteca padrão:
- Recursos extras: Frameworks oferecem middleware, validação de request, renderização de templates, binding de JSON, entre outros. A biblioteca padrão não fornece esses utilitários, exigindo que você implemente ou use outras bibliotecas.
- Roteamento avançado: Embora o novo padrão cubra muitos casos, alguns cenários como rotas com parâmetros opcionais, regex, ou priorização complexa podem ser mais difíceis de expressar.
- Ecossistema: Frameworks têm uma comunidade maior e muitos exemplos, plugins e integrações prontas.
Vejamos um exemplo comparativo simples: uma API com duas rotas GET e POST.
Com a biblioteca padrão:
mux := http.NewServeMux()
mux.HandleFunc("GET /api/items", listItems)
mux.HandleFunc("POST /api/items", createItem)Com Gin:
r := gin.Default()
r.GET("/api/items", listItems)
r.POST("/api/items", createItem)Como você pode ver, a sintaxe é muito similar. A diferença aparece quando você precisa de middleware, binding de JSON, etc. No Gin, você pode fazer r.POST("/api/items", middleware, handler) e usar c.ShouldBindJSON(&item) para parsear o corpo da requisição. Na biblioteca padrão, você teria que fazer isso manualmente.
Quando escolher a biblioteca padrão:
- Projetos pequenos e médios onde a simplicidade é prioridade.
- APIs que não precisam de muitos recursos extras.
- Quando você quer minimizar dependências e manter o controle total.
Quando usar um framework:
- Projetos grandes com muitos endpoints e necessidade de organização.
- Quando você precisa de middleware complexo (autenticação, logging, CORS, etc.).
- Quando você quer produtividade imediata com funcionalidades prontas.
Em resumo, a nova ServeMux é uma excelente opção para muitos casos, mas frameworks continuam sendo relevantes para projetos que exigem mais recursos. A decisão deve ser baseada nas necessidades específicas do seu projeto.
Boas práticas
Ao usar o novo roteamento, siga estas boas práticas para manter seu código limpo e eficiente:
- Use métodos HTTP nos padrões sempre que possível, para evitar verificações manuais e melhorar a clareza.
- Valide os parâmetros no handler (ex.: converter para int) e retorne erros apropriados (400 Bad Request).
- Organize as rotas em grupos usando sub-muxes (via
http.StripPrefixouhttp.NewServeMux) para modularizar seu código. - Prefira nomes descritivos para os parâmetros (ex.:
{id}em vez de{x}). - Evite padrões ambíguos que possam conflitar; o
ServeMuxresolve conflitos pela especificidade, mas é melhor evitar. - Teste suas rotas usando
httptestpara garantir que os padrões correspondem corretamente.
Referências
- Go 1.22 Routing Enhancements (Blog Oficial)
- Documentação oficial do ServeMux
- Método PathValue na documentação
- Notas de lançamento do Go 1.22
- Chi Router (comparação)
- Gin Framework (comparação)
- Go Wiki sobre roteadores HTTP
Exercícios
- Crie um servidor com uma rota
GET /hello/{name}que responda "Olá, {name}!". User.PathValuepara extrair o nome. - Implemente uma rota
POST /api/itemsque retorne o status 201 e o corpo "Item criado". Use o padrão com método. - Crie uma rota
GET /files/{path...}que retorne o caminho capturado. User.PathValue("path"). - Escreva um servidor que registre as rotas
GET /usersePOST /userspara listar e criar usuários (simule com mensagens). - Crie uma rota
PUT /users/{id}que valide se oidé um número inteiro. Se não for, retorne 400. Caso contrário, retorne "Atualizando usuário {id}".
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /hello/{name}", func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("name")
fmt.Fprintf(w, "Olá, %s!", name)
})
http.ListenAndServe(":8080", mux)
}mux.HandleFunc("POST /api/items", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "Item criado")
})mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
path := r.PathValue("path")
fmt.Fprintf(w, "Caminho: %s", path)
})package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Lista de usuários")
})
mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Criar usuário")
})
http.ListenAndServe(":8080", mux)
}mux.HandleFunc("PUT /users/{id}", func(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "ID inválido", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "Atualizando usuário %d", id)
})