Golang

Artigo 28 — Criando um servidor HTTP com net/http Já leu

11 min de leitura

Artigo 28 — Criando um servidor HTTP com net/http
Artigo 28 — Criando um servidor HTTP com net/http O servidor HTTP que já vem na caixa Uma das razões pelas quais Go se tornou a linguagem preferida para

Artigo 28 — Criando um servidor HTTP com net/http

Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 5 — I/O, HTTP e APIs


O servidor HTTP que já vem na caixa

Uma das razões pelas quais Go se tornou a linguagem preferida para backends é a qualidade do suporte HTTP na biblioteca padrão. Sem instalar nenhuma dependência externa, é possível criar um servidor HTTP production-ready, com roteamento, middlewares, timeouts, TLS e suporte a HTTP/2.

O Go 1.22 trouxe melhorias significativas ao roteador padrão — suporte a métodos HTTP e parâmetros de caminho diretamente nos padrões de rota — reduzindo ainda mais a necessidade de bibliotecas externas para casos simples.


O mínimo funcional

Um servidor HTTP em Go começa com três conceitos: o Handler, o ServeMux e o Server:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    // Handler mais simples possível
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Olá, mundo!")
    })

    fmt.Println("Servidor em http://localhost:8080")
    http.ListenAndServe(":8080", nil) // nil usa o DefaultServeMux
}

Embora funcional, esse código usa o DefaultServeMux global — um estado compartilhado que torna testes difíceis. A forma recomendada é criar um ServeMux explícito:

package main

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

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

    mux.HandleFunc("/", handlerRaiz)
    mux.HandleFunc("/saude", handlerSaude)

    servidor := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    log.Println("Servidor iniciado em :8080")
    if err := servidor.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}

func handlerRaiz(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "API funcionando")
}

func handlerSaude(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    fmt.Fprintln(w, `{"status":"ok"}`)
}

Os timeouts são essenciais em produção. Sem eles, conexões lentas ou maliciosas podem consumir recursos indefinidamente.


A interface Handler

O coração do sistema HTTP do Go é a interface http.Handler:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Qualquer tipo que implemente ServeHTTP pode ser usado como handler. http.HandlerFunc é um adaptador que converte funções com a assinatura correta em handlers:

// HandlerFunc é definido na biblioteca padrão como:
type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

Isso permite criar handlers como structs com estado:

type HandlerProdutos struct {
    servico *ServicoProdutos
    logger  *slog.Logger
}

func (h *HandlerProdutos) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        h.listar(w, r)
    case http.MethodPost:
        h.criar(w, r)
    default:
        http.Error(w, "método não permitido", http.StatusMethodNotAllowed)
    }
}

Roteamento com Go 1.22

O Go 1.22 adicionou suporte a métodos HTTP e parâmetros de caminho diretamente nos padrões do ServeMux:

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strconv"
)

type Produto struct {
    ID    int     `json:"id"`
    Nome  string  `json:"nome"`
    Preco float64 `json:"preco"`
}

var produtos = []Produto{
    {ID: 1, Nome: "Teclado", Preco: 250.0},
    {ID: 2, Nome: "Mouse", Preco: 120.0},
    {ID: 3, Nome: "Monitor", Preco: 1500.0},
}

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

    // Método + caminho — Go 1.22+
    mux.HandleFunc("GET /produtos", listarProdutos)
    mux.HandleFunc("POST /produtos", criarProduto)
    mux.HandleFunc("GET /produtos/{id}", buscarProduto)
    mux.HandleFunc("PUT /produtos/{id}", atualizarProduto)
    mux.HandleFunc("DELETE /produtos/{id}", deletarProduto)

    fmt.Println("API em :8080")
    http.ListenAndServe(":8080", mux)
}

func listarProdutos(w http.ResponseWriter, r *http.Request) {
    responderJSON(w, http.StatusOK, produtos)
}

func buscarProduto(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        http.Error(w, "id inválido", http.StatusBadRequest)
        return
    }

    for _, p := range produtos {
        if p.ID == id {
            responderJSON(w, http.StatusOK, p)
            return
        }
    }

    http.Error(w, "produto não encontrado", http.StatusNotFound)
}

func criarProduto(w http.ResponseWriter, r *http.Request) {
    var p Produto
    if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
        http.Error(w, "corpo inválido: "+err.Error(), http.StatusBadRequest)
        return
    }

    p.ID = len(produtos) + 1
    produtos = append(produtos, p)
    responderJSON(w, http.StatusCreated, p)
}

func atualizarProduto(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        http.Error(w, "id inválido", http.StatusBadRequest)
        return
    }

    var atualizado Produto
    if err := json.NewDecoder(r.Body).Decode(&atualizado); err != nil {
        http.Error(w, "corpo inválido", http.StatusBadRequest)
        return
    }

    for i, p := range produtos {
        if p.ID == id {
            atualizado.ID = id
            produtos[i] = atualizado
            responderJSON(w, http.StatusOK, atualizado)
            return
        }
    }

    http.Error(w, "produto não encontrado", http.StatusNotFound)
}

func deletarProduto(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        http.Error(w, "id inválido", http.StatusBadRequest)
        return
    }

    for i, p := range produtos {
        if p.ID == id {
            produtos = append(produtos[:i], produtos[i+1:]...)
            w.WriteHeader(http.StatusNoContent)
            return
        }
    }

    http.Error(w, "produto não encontrado", http.StatusNotFound)
}

func responderJSON(w http.ResponseWriter, status int, dados any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(dados)
}

Middlewares: comportamento transversal

Middleware é uma função que envolve um handler, adicionando comportamento antes e/ou depois de sua execução. O padrão idiomático em Go retorna um http.Handler:

package main

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

// Middleware de logging
func comLogging(logger *slog.Logger, prox http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        inicio := time.Now()

        // Wrapper para capturar o status code
        rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}

        prox.ServeHTTP(rw, r)

        logger.Info("requisição",
            "método", r.Method,
            "caminho", r.URL.Path,
            "status", rw.status,
            "duração", time.Since(inicio),
            "ip", r.RemoteAddr,
        )
    })
}

// Middleware de recuperação de pânico
func comRecuperacao(prox http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                slog.Error("pânico recuperado", "erro", err, "caminho", r.URL.Path)
                http.Error(w, "erro interno do servidor", http.StatusInternalServerError)
            }
        }()
        prox.ServeHTTP(w, r)
    })
}

// Middleware de CORS
func comCORS(origemPermitida string, prox http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", origemPermitida)
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }

        prox.ServeHTTP(w, r)
    })
}

// Middleware de autenticação por token
func comAutenticacao(token string, prox http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        auth := r.Header.Get("Authorization")
        if auth != "Bearer "+token {
            http.Error(w, "não autorizado", http.StatusUnauthorized)
            return
        }
        prox.ServeHTTP(w, r)
    })
}

// responseWriter captura o status code
type responseWriter struct {
    http.ResponseWriter
    status int
}

func (rw *responseWriter) WriteHeader(status int) {
    rw.status = status
    rw.ResponseWriter.WriteHeader(status)
}

func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/dados", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"dados":"protegidos"}`))
    })

    // Encadeamento de middlewares — da camada mais externa para a mais interna
    var handler http.Handler = mux
    handler = comAutenticacao("meu-token-secreto", handler)
    handler = comCORS("*", handler)
    handler = comRecuperacao(handler)
    handler = comLogging(logger, handler)

    servidor := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    logger.Info("servidor iniciado", "addr", ":8080")
    servidor.ListenAndServe()
}

Lendo parâmetros da requisição

func handlerBusca(w http.ResponseWriter, r *http.Request) {
    // Query parameters: /busca?q=teclado&pagina=2&limite=10
    q := r.URL.Query().Get("q")
    pagina, _ := strconv.Atoi(r.URL.Query().Get("pagina"))
    limite, _ := strconv.Atoi(r.URL.Query().Get("limite"))

    if pagina <= 0 {
        pagina = 1
    }
    if limite <= 0 || limite > 100 {
        limite = 20
    }

    fmt.Fprintf(w, "busca: q=%s pagina=%d limite=%d", q, pagina, limite)
}

func handlerFormulario(w http.ResponseWriter, r *http.Request) {
    // Form data (application/x-www-form-urlencoded)
    if err := r.ParseForm(); err != nil {
        http.Error(w, "formulário inválido", http.StatusBadRequest)
        return
    }

    nome := r.FormValue("nome")
    email := r.FormValue("email")
    fmt.Fprintf(w, "nome=%s email=%s", nome, email)
}

func handlerHeaders(w http.ResponseWriter, r *http.Request) {
    // Lendo headers
    contentType := r.Header.Get("Content-Type")
    userAgent := r.Header.Get("User-Agent")
    token := r.Header.Get("Authorization")

    // Escrevendo headers na resposta
    w.Header().Set("X-Request-ID", "abc-123")
    w.Header().Set("Cache-Control", "no-cache")

    fmt.Fprintf(w, "content-type=%s user-agent=%s token=%s",
        contentType, userAgent, token)
}

Servindo arquivos estáticos

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

    // API
    mux.HandleFunc("GET /api/versao", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(`{"versao":"1.0.0"}`))
    })

    // Arquivos estáticos — serve o diretório "./public"
    fs := http.FileServer(http.Dir("./public"))
    mux.Handle("/static/", http.StripPrefix("/static/", fs))

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

Encerramento gracioso

package main

import (
    "context"
    "log/slog"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
        time.Sleep(100 * time.Millisecond) // simula processamento
        w.Write([]byte("ok"))
    })

    servidor := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    // Inicia em goroutine separada
    go func() {
        slog.Info("servidor iniciado", "addr", ":8080")
        if err := servidor.ListenAndServe(); err != http.ErrServerClosed {
            slog.Error("erro no servidor", "err", err)
            os.Exit(1)
        }
    }()

    // Aguarda sinal de encerramento
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    slog.Info("encerrando servidor...")

    ctx, cancelar := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancelar()

    if err := servidor.Shutdown(ctx); err != nil {
        slog.Error("erro no shutdown", "err", err)
        os.Exit(1)
    }

    slog.Info("servidor encerrado com sucesso")
}

Testando handlers HTTP

package main

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestListarProdutos(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/produtos", nil)
    rr := httptest.NewRecorder()

    listarProdutos(rr, req)

    if rr.Code != http.StatusOK {
        t.Errorf("status esperado %d, obtido %d", http.StatusOK, rr.Code)
    }

    var resultado []Produto
    if err := json.NewDecoder(rr.Body).Decode(&resultado); err != nil {
        t.Fatalf("erro ao decodificar resposta: %v", err)
    }

    if len(resultado) == 0 {
        t.Error("esperava ao menos um produto")
    }
}

func TestBuscarProdutoNaoEncontrado(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/produtos/999", nil)
    req.SetPathValue("id", "999") // simula parâmetro de rota
    rr := httptest.NewRecorder()

    buscarProduto(rr, req)

    if rr.Code != http.StatusNotFound {
        t.Errorf("status esperado %d, obtido %d",
            http.StatusNotFound, rr.Code)
    }
}

O pacote httptest é fundamental para testes de handlers — permite criar requisições e respostas falsas sem iniciar um servidor real.


Resumo do que foi coberto

Este artigo apresentou o servidor HTTP do Go em profundidade: o mínimo funcional com http.Server e timeouts, a interface Handler, roteamento moderno com Go 1.22 incluindo métodos e parâmetros de caminho, middlewares de logging, recuperação de pânico, CORS e autenticação, leitura de query parameters, form data e headers, servindo arquivos estáticos, encerramento gracioso com Shutdown e testes de handlers com httptest. O próximo artigo explora roteamento avançado com bibliotecas externas.


Referências e leituras complementares

  • Documentação do pacote net/http — Referência completa do servidor e cliente HTTP. https://pkg.go.dev/net/http

  • Go Blog: HTTP/2 — Suporte nativo a HTTP/2 em servidores Go. https://go.dev/blog/h2push

  • Go 1.22 Release Notes — Enhanced routing — Detalhes sobre as melhorias de roteamento. https://go.dev/blog/routing-enhancements

  • Go by Example: HTTP Servers — Exemplos práticos de servidores HTTP. https://gobyexample.com/http-servers

  • Documentação httptest — Referência do pacote de testes HTTP. https://pkg.go.dev/net/http/httptest

  • Let's Encrypt com Go — Configurando TLS automático com certmagic ou autocert. https://pkg.go.dev/golang.org/x/crypto/acme/autocert


Próximo artigo: Artigo 29 — Roteamento avançado com Gorilla Mux ou Chi**


you asked

Sim


claude response

Comentários

Mais em Golang

Variáveis, tipos primitivos e declaração curta
Variáveis, tipos primitivos e declaração curta

&nbsp; O sistema de tipos do Go &eacute; sua primeira linha de defesa!&nbsp;...

Goroutines: concorrência leve e o modelo de execução do Go
Goroutines: concorrência leve e o modelo de execução do Go

Se existe um recurso que mais distingue Go de outras linguagens modernas, é s...

Artigo 40 — Variáveis de ambiente e configuração com Viper
Artigo 40 — Variáveis de ambiente e configuração com Viper

Artigo 40 — Variáveis de ambiente e configuração com Viper Configuração: o el...