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étodo

Com 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.StripPrefix ou http.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 ServeMux resolve conflitos pela especificidade, mas é melhor evitar.
  • Teste suas rotas usando httptest para garantir que os padrões correspondem corretamente.

Referências

Exercícios

  1. Crie um servidor com uma rota GET /hello/{name} que responda "Olá, {name}!". Use r.PathValue para extrair o nome.
  2. ✓ Resposta:
    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)
    }
  3. Implemente uma rota POST /api/items que retorne o status 201 e o corpo "Item criado". Use o padrão com método.
  4. ✓ Resposta:
    mux.HandleFunc("POST /api/items", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusCreated)
        fmt.Fprintln(w, "Item criado")
    })
  5. Crie uma rota GET /files/{path...} que retorne o caminho capturado. Use r.PathValue("path").
  6. ✓ Resposta:
    mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
        path := r.PathValue("path")
        fmt.Fprintf(w, "Caminho: %s", path)
    })
  7. Escreva um servidor que registre as rotas GET /users e POST /users para listar e criar usuários (simule com mensagens).
  8. ✓ Resposta:
    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)
    }
  9. Crie uma rota PUT /users/{id} que valide se o id é um número inteiro. Se não for, retorne 400. Caso contrário, retorne "Atualizando usuário {id}".
  10. ✓ Resposta:
    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)
    })