O pacote net/http do Go sempre forneceu um roteador básico, o http.ServeMux, mas até a versão 1.21 ele era limitado: não suportava métodos HTTP específicos, nem parâmetros de caminho de forma nativa. Desenvolvedores frequentemente recorriam a bibliotecas de terceiros como gorilla/mux ou chi para obter esses recursos. A partir do Go 1.22, o ServeMux foi significativamente aprimorado, introduzindo um novo padrão de roteamento que permite especificar métodos, capturar parâmetros de caminho e até mesmo usar curingas. Isso torna o roteamento nativo muito mais poderoso e reduz a necessidade de dependências externas para muitos casos de uso.

Nesta aula, vamos mergulhar nas novas capacidades do http.ServeMux, entender como escrever rotas expressivas e comparar com abordagens baseadas em frameworks. Ao final, você terá uma compreensão sólida para escolher entre o roteamento nativo e bibliotecas externas em seus projetos.

Padrões de rota (Go 1.22+)

Antes do Go 1.22, o ServeMux usava padrões baseados em prefixo, sem suporte a métodos ou parâmetros. Agora, o padrão de rota pode incluir o método HTTP e segmentos de caminho com curingas. A sintaxe básica é "MÉTODO /caminho/segmento", onde o método é opcional (se omitido, a rota responde a qualquer método). Os padrões podem conter curingas como {nome} para capturar um segmento, ou {nome...} para capturar o restante do caminho.

Vamos ver um exemplo simples de registro de rotas com métodos e curingas:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    mux := http.NewServeMux()

    // Rota exata para GET /hello
    mux.HandleFunc("GET /hello", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Olá, mundo!")
    })

    // Rota com curinga {name} para GET /users/{name}
    mux.HandleFunc("GET /users/{name}", func(w http.ResponseWriter, r *http.Request) {
        name := r.PathValue("name")
        fmt.Fprintf(w, "Usuário: %s\n", name)
    })

    // Rota curinga para /files/{path...} captura o restante
    mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
        path := r.PathValue("path")
        fmt.Fprintf(w, "Caminho: %s\n", path)
    })

    http.ListenAndServe(":8080", mux)
}

O curinga {name} captura exatamente um segmento (não pode conter barra), enquanto {path...} captura o restante do caminho, incluindo barras. Isso permite criar rotas flexíveis sem precisar de bibliotecas externas.

Métodos

Uma das maiores adições é a capacidade de associar um método HTTP a um padrão. Antes, era necessário verificar manualmente r.Method dentro do handler. Agora, o roteador cuida disso, retornando 405 Method Not Allowed se o método não corresponder, e 404 se o padrão não existir. Isso torna o código mais limpo e seguro.

É importante notar que, se você especificar um método, a rota só responderá àquele método. Para rotas que aceitam qualquer método, basta omitir o método no padrão. Exemplo:

mux.HandleFunc("/api/data", func(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        // tratar GET
    case http.MethodPost:
        // tratar POST
    default:
        w.WriteHeader(http.StatusMethodNotAllowed)
    }
})

Com o novo padrão, você pode registrar handlers separados para cada método, o que melhora a organização e elimina a necessidade de switch:

mux.HandleFunc("GET /api/data", getData)
mux.HandleFunc("POST /api/data", postData)
mux.HandleFunc("PUT /api/data", putData)

Se um método não for suportado, o ServeMux retorna automaticamente o status 405. Isso é um grande ganho de produtividade e consistência.

Path parameters

Os path parameters são acessados através do método r.PathValue("nome") no handler. Isso substitui a necessidade de analisar a URL manualmente com strings.Split ou expressões regulares. Os parâmetros são extraídos de forma segura e tipada (sempre string), e você pode convertê-los conforme necessário.

Vamos ver um exemplo completo com múltiplos parâmetros e validação:

mux.HandleFunc("GET /products/{category}/{id}", func(w http.ResponseWriter, r *http.Request) {
    category := r.PathValue("category")
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "ID inválido", http.StatusBadRequest)
        return
    }
    fmt.Fprintf(w, "Categoria: %s, ID: %d\n", category, id)
})

Além disso, o ServeMux garante que os parâmetros estejam preenchidos na rota correspondente, então não é necessário verificar se a chave existe. Isso reduz erros comuns. Para rotas curinga como {path...}, o valor pode ser vazio se não houver segmentos restantes, mas o padrão deve ser usado com cuidado para não capturar rotas indesejadas.

Comparação com frameworks

Frameworks como gin, echo e chi oferecem recursos adicionais, como middleware, validação, agrupamento de rotas, e integração com contextos enriquecidos. No entanto, o ServeMux nativo agora cobre a maioria das necessidades básicas de roteamento com menos dependências e menor overhead.

Vamos comparar um exemplo de rota com parâmetros em chi e no ServeMux nativo:

// Usando chi
r := chi.NewRouter()
r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    // ...
})

// Usando ServeMux nativo (Go 1.22+)
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // ...
})

A sintaxe é quase idêntica, mas o nativo não requer importações adicionais. Para projetos simples a médios, o ServeMux é suficiente. No entanto, se você precisa de middleware reutilizável, roteamento aninhado complexo ou recursos como validação automática, frameworks como chi ainda podem ser vantajosos. A escolha depende da complexidade do projeto e da preferência da equipe.

Boas práticas

Ao usar o novo ServeMux, é importante:

  • Definir métodos explicitamente para evitar rotas acidentais.
  • Usar parâmetros nomeados em vez de expressões regulares para legibilidade.
  • Evitar curingas excessivos, pois podem capturar rotas inesperadas.
  • Manter os handlers pequenos e delegar a lógica para funções separadas.
  • Utilizar http.Server com timeouts para produção.

Referências

Exercícios

  1. Crie um servidor HTTP com ServeMux que tenha uma rota GET /ping respondendo "pong".

    ✓ Resposta:
    package main
    
    import (
        "fmt"
        "net/http"
    )
    
    func main() {
        mux := http.NewServeMux()
        mux.HandleFunc("GET /ping", func(w http.ResponseWriter, r *http.Request) {
            fmt.Fprintln(w, "pong")
        })
        http.ListenAndServe(":8080", mux)
    }
    
  2. Adicione uma rota POST /echo que leia o corpo da requisição e o devolva como resposta.

    ✓ Resposta:
    mux.HandleFunc("POST /echo", func(w http.ResponseWriter, r *http.Request) {
        body, err := io.ReadAll(r.Body)
        if err != nil {
            http.Error(w, "Erro ao ler corpo", http.StatusInternalServerError)
            return
        }
        w.Write(body)
    })
    
  3. Implemente uma rota GET /users/{id} que retorna o ID recebido como parâmetro.

    ✓ Resposta:
    mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")
        fmt.Fprintf(w, "ID: %s\n", id)
    })
    
  4. Crie uma rota GET /files/{path...} que retorna o caminho completo capturado.

    ✓ Resposta:
    mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
        path := r.PathValue("path")
        fmt.Fprintf(w, "Caminho: %s\n", path)
    })
    
  5. Escreva um servidor que responda 200 OK para qualquer método em /health, mas retorne 405 para métodos não permitidos em outras rotas.

    ✓ Resposta:
    mux := http.NewServeMux()
    mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
    })
    mux.HandleFunc("/other", func(w http.ResponseWriter, r *http.Request) {
        // Esta rota só aceita GET, mas se você tentar POST, o ServeMux retorna 405 automaticamente.
        fmt.Fprintln(w, "OK")
    })
    

    Nota: Para que o ServeMux retorne 405 automaticamente, você deve especificar o método no padrão. No exemplo acima, /health aceita qualquer método, então não retornará 405. Para testar 405, defina um método específico, ex.: mux.HandleFunc("GET /other", ...).

Observações finais

O aprimoramento do ServeMux no Go 1.22 é um passo significativo para tornar a biblioteca padrão mais competitiva com frameworks. Para projetos que não exigem funcionalidades avançadas, o uso do ServeMux nativo reduz dependências e simplifica o deploy. No entanto, frameworks ainda oferecem conveniências como middleware reutilizável e validação, então avalie as necessidades do seu projeto. Com o conhecimento desta aula, você está apto a escolher a melhor abordagem.