Pular para o conteúdo

Trilha de Go · parte 9 de 37

Servidor HTTP Nativo Moderno em Go (1.22+)

Como criar APIs REST completas só com a biblioteca padrão, usando o roteador http.ServeMux do Go 1.22+ com verbos e parâmetros.

Publicada em · 6 min de leitura

Tamanho da fonte:
Largura da coluna:

Servidor HTTP Nativo Moderno em Go (1.22+)

Por mais de uma década, construir uma API RESTful idiomática em Go com a biblioteca padrão era um exercício frustrante. O antigo http.ServeMux só suportava correspondência simples de prefixos de URL: não distinguia verbos HTTP (GET vs POST), não extraía parâmetros de rota (/users/{id}) e exigia que quase todo projeto adotasse bibliotecas externas como Gorilla Mux, Chi, Echo ou Gin.

A partir do Go 1.22, a equipe do Go reformulou completamente o http.ServeMux, introduzindo roteamento declarativo por método, captura nativa de parâmetros com r.PathValue() e wildcards. Com essa revolução, somada ao suporte a sistemas de arquivos embutidos (embed.FS), a biblioteca padrão do Go tornou-se mais do que suficiente para a esmagadora maioria das APIs corporativas, eliminando dependências externas desnecessárias.


1. O Novo http.ServeMux: Recursos e Sintaxe

O http.ServeMux moderno permite registrar padrões que combinam o verbo HTTP, segmentos exatos de caminho, variáveis de rota e curingas residuais.

Roteamento Moderno ServeMux
figura 1 · Roteamento Moderno ServeMux

Padrões de Roteamento Suportados

PadrãoSintaxe de RegistroComo Obter o Valor
Método + Caminho Exato"GET /healthz"—
Método + Path Parameter"GET /users/{id}"id := r.PathValue("id")
Múltiplas Variáveis"GET /orgs/{org}/repos/{repo}"r.PathValue("org"), r.PathValue("repo")
Wildcard Residual"GET /files/{filepath...}"sub := r.PathValue("filepath")
Hostname Específico"api.empresa.com/v1/"Match condicionado ao header Host

Tratamento Automático de 405 Method Not Allowed

No Go 1.22+, se uma rota foi registrada para POST /items, e o cliente efetuar uma requisição GET /items, o ServeMux automaticamente responderá com HTTP 405 Method Not Allowed e preencherá o cabeçalho Allow: POST, respeitando a especificação RFC 9110 sem uma única linha de código extra!


2. Implementando uma API REST Nativa

Veja como estruturar rotas e handlers de forma limpa, fortemente tipada e com tratamento de erros sem nenhuma dependência externa:

package main

import (
	"encoding/json"
	"errors"
	"log/slog"
	"net/http"
	"strconv"
)

type Product struct {
	ID    int64   `json:"id,string"`
	Title string  `json:"title"`
	Price float64 `json:"price"`
}

type ProductHandler struct {
	// Injetar dependências de serviço/repositório aqui
}

func (h *ProductHandler) RegisterRoutes(mux *http.ServeMux) {
	mux.HandleFunc("GET /api/v1/products", h.List)
	mux.HandleFunc("GET /api/v1/products/{id}", h.GetByID)
	mux.HandleFunc("POST /api/v1/products", h.Create)
	mux.HandleFunc("DELETE /api/v1/products/{id}", h.Delete)
}

func (h *ProductHandler) GetByID(w http.ResponseWriter, r *http.Request) {
	// Extração nativa do parâmetro {id}
	idStr := r.PathValue("id")
	id, err := strconv.ParseInt(idStr, 10, 64)
	if err != nil || id <= 0 {
		http.Error(w, `{"error":"id de produto inválido"}`, http.StatusBadRequest)
		return
	}

	product := Product{ID: id, Title: "Teclado Mecânico Custom", Price: 550.00}
	
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(product)
}

func (h *ProductHandler) Create(w http.ResponseWriter, r *http.Request) {
	var input Product
	// Decodificação direta sem buffer intermediário
	if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
		http.Error(w, `{"error":"payload JSON inválido"}`, http.StatusBadRequest)
		return
	}

	input.ID = 100201 // Simula inserção em banco de dados

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(input)
}

func (h *ProductHandler) List(w http.ResponseWriter, r *http.Request) {
	// Query params continuam acessíveis via r.URL.Query()
	limit := r.URL.Query().Get("limit")
	_ = limit

	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode([]Product{})
}

func (h *ProductHandler) Delete(w http.ResponseWriter, r *http.Request) {
	id := r.PathValue("id")
	_ = id
	w.WriteHeader(http.StatusNoContent)
}

3. Servindo Arquivos Estáticos e SPAs com //go:embed

Uma das maiores forças do Go em infraestrutura e DevOps é a capacidade de compilar toda a aplicação — incluindo interfaces frontend (HTML, JavaScript, CSS, imagens e Swagger UI) — em um único binário executável autocontido, eliminando dependências de arquivos externos no filesystem do contêiner Docker.

Servindo Assets Estáticos com embed.FS
figura 2 · Servindo Assets Estáticos com embed.FS
package main

import (
	"embed"
	"io/fs"
	"log"
	"net/http"
)

// Diretiva que embute o diretório dist/ do React/Vue no binário Go durante o build
//
//go:embed all:dist
var frontendDist embed.FS

func RegisterStaticRoutes(mux *http.ServeMux) {
	// Cria um sub-sistema de arquivos removendo a pasta raiz "dist"
	distFS, err := fs.Sub(frontendDist, "dist")
	if err != nil {
		log.Fatalf("falha ao carregar assets embutidos: %v", err)
	}

	// FileServerFS é o método nativo moderno para servir instâncias de fs.FS
	fileServer := http.FileServerFS(distFS)

	// Captura todas as rotas estáticas
	mux.Handle("GET /static/{filepath...}", http.StripPrefix("/static/", fileServer))
}

4. Configuração Blindada do http.Server para Produção

Assim como não se deve usar o http.DefaultClient, nunca suba um servidor com o método simplista http.ListenAndServe(":8080", mux). Essa função utiliza internamente uma instância padrão de http.Server com todos os timeouts zerados, deixando o servidor suscetível ao clássico ataque de exaustão de conexões Slowloris.

package server

import (
	"log/slog"
	"net/http"
	"time"
)

// NewSecureServer instancia o servidor HTTP com limites estritos de segurança
func NewSecureServer(addr string, handler http.Handler) *http.Server {
	return &http.Server{
		Addr:    addr,
		Handler: handler,

		// DEFESA VITAL CONTRA SLOWLORIS:
		// Limita o tempo que o cliente tem para enviar os headers da requisição.
		ReadHeaderTimeout: 2 * time.Second,

		// Tempo máximo para leitura completa da requisição (headers + body)
		ReadTimeout: 5 * time.Second,

		// Tempo máximo do início da escrita da resposta até o fim da transmissão
		WriteTimeout: 10 * time.Second,

		// Tempo que uma conexão TCP Keep-Alive pode permanecer ociosa antes de fechar
		IdleTimeout: 120 * time.Second,

		// Limita o tamanho dos cabeçalhos a 1 Megabyte (evita estouro de RAM com cookies gigantes)
		MaxHeaderBytes: 1 << 20,

		// Integração com log estruturado nativo
		ErrorLog: slog.NewLogLogger(slog.Default().Handler(), slog.LevelError),
	}
}

5. Middlewares Nativos sem Frameworks

Em Go, qualquer função que respeite a assinatura func(http.Handler) http.Handler atua perfeitamente como middleware. Podemos compor middlewares de recuperação de pânico, logging e cabeçalhos de segurança de forma extremamente limpa.

package middleware

import (
	"log/slog"
	"net/http"
	"time"
)

// Middleware é o contrato padrão de interceptação HTTP em Go
type Middleware func(http.Handler) http.Handler

// CreateStack encadeia múltiplos middlewares em ordem de execução
func CreateStack(xs ...Middleware) Middleware {
	return func(next http.Handler) http.Handler {
		for i := len(xs) - 1; i >= 0; i-- {
			next = xs[i](next)
		}
		return next
	}
}

// Recovery captura pânicos não tratados e retorna 500 sem derrubar o processo
func Recovery(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		defer func() {
			if rec := recover(); rec != nil {
				slog.Error("Pânico não tratado capturado", slog.Any("panic", rec))
				http.Error(w, `{"error":"erro interno do servidor"}`, http.StatusInternalServerError)
			}
		}()
		next.ServeHTTP(w, r)
	})
}

// RequestLogger gera logs estruturados com tempo de execução
func RequestLogger(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		slog.Info("HTTP Request Concluída",
			slog.String("method", r.Method),
			slog.String("path", r.URL.Path),
			slog.Duration("latency", time.Since(start)),
		)
	})
}

Uso no main.go:

mux := http.NewServeMux()
productHandler.RegisterRoutes(mux)

stack := middleware.CreateStack(
    middleware.Recovery,
    middleware.RequestLogger,
)

server := server.NewSecureServer(":8080", stack(mux))

6. Comparativo: Stdlib Moderna vs Roteadores Externos

Requisitohttp.ServeMux (Go 1.22+)Chi / GorillaGin / Fiber
Dependências ExternasZeroBaixaAlta
Roteamento por Verbo HTTPSim (GET /users)SimSim
Extração de Path ParamsSim (r.PathValue)SimSim
Wildcards ResiduaisSim ({path...})SimSim
Regex em RotasNão (requer validação no handler)SimSim
Alocação de MemóriaMínima (Radix Tree otimizada)MínimaOtimizada via Radix
Longevidade e ManutençãoGarantia de compatibilidade Go 1Sujeito a abandonoSujeito a breaking changes

Veredito de Arquitetura: Para novos projetos corporativos em Go 1.22+, comece sempre com a biblioteca padrão. Adote roteadores como Chi apenas se sua aplicação depender pesadamente de rotas com expressões regulares complexas ou agrupamento hierárquico avançado de sub-roteadores com middlewares específicos por grupo.


7. Checklist de Produção para Servidores HTTP

  • Abandonar frameworks pesados e adotar http.ServeMux nativo do Go 1.22+.
  • Configurar explicitamente ReadHeaderTimeout para anular ataques de Slowloris.
  • Configurar ReadTimeout, WriteTimeout e IdleTimeout.
  • Definir MaxHeaderBytes para proteger contra estouro de memória por cabeçalhos abusivos.
  • Utilizar r.PathValue("chave") para extrair variáveis limpas de URL.
  • Implementar middleware de Recovery para capturar pânicos inesperados sem abortar a instância.
  • Integrar com Graceful Shutdown e Signal Handling para aguardar requisições em voo no encerramento de pods Kubernetes.

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?