HTTP server com net/http

1. Introdução ao pacote net/http

O pacote net/http é a espinha dorsal da construção de servidores HTTP em Go. Diferentemente de outras linguagens que exigem frameworks externos para tarefas básicas, Go oferece uma biblioteca padrão robusta e completa para criar servidores HTTP de produção.

A estrutura mais simples de um servidor HTTP utiliza http.ListenAndServe, que aceita um endereço (como ":8080") e um handler. O handler é qualquer valor que implemente a interface http.Handler, que exige apenas um método: ServeHTTP(ResponseWriter, *Request).

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Olá, mundo!"))
    })
    http.ListenAndServe(":8080", nil)
}

A diferença entre Handler e HandlerFunc é sutil mas importante. http.Handler é uma interface, enquanto http.HandlerFunc é um tipo de função que implementa essa interface automaticamente. Isso permite que funções comuns sejam usadas como handlers sem necessidade de structs especiais.

2. Trabalhando com multiplexadores (mux)

O multiplexador (ou roteador) decide qual handler atenderá cada requisição. O http.DefaultServeMux é o mux global usado quando passamos nil para ListenAndServe. Para mais controle, usamos http.NewServeMux().

mux := http.NewServeMux()
mux.HandleFunc("/api/hello", helloHandler)
mux.Handle("/api/admin", adminMiddleware(adminHandler))
http.ListenAndServe(":8080", mux)

A diferença entre Handle e HandleFunc é que Handle espera um http.Handler (interface), enquanto HandleFunc aceita uma função com assinatura func(ResponseWriter, *Request).

Com Go 1.22+, o pattern matching foi modernizado para suportar métodos HTTP e variáveis de path:

mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", getUserByID)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)

3. Handlers personalizados e middlewares

Handlers como structs são úteis quando precisamos de estado ou dependências injetadas:

type UserHandler struct {
    db *sql.DB
}

func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // lógica que usa h.db
}

Middlewares são funções que recebem e retornam http.Handler, permitindo empilhar comportamentos:

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
    })
}

func compressionMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
            w.Header().Set("Content-Encoding", "gzip")
            gz := gzip.NewWriter(w)
            defer gz.Close()
            next.ServeHTTP(gzipResponseWriter{Writer: gz, ResponseWriter: w}, r)
            return
        }
        next.ServeHTTP(w, r)
    })
}

4. Manipulação de requisições e respostas

A leitura de parâmetros varia conforme o tipo:

// Query parameters
name := r.URL.Query().Get("name")

// Path variables (Go 1.22+)
userID := r.PathValue("id")

// Form data
email := r.FormValue("email")

Cabeçalhos são acessados de forma similar em requisições e respostas:

// Lendo cabeçalhos da requisição
authToken := r.Header.Get("Authorization")

// Escrevendo cabeçalhos na resposta
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Custom-Header", "value")

Para escrever respostas, temos três abordagens principais:

// w.Write() - escreve bytes diretamente
w.Write([]byte("texto simples"))

// w.WriteHeader() - define o status code (deve ser chamado antes de Write)
w.WriteHeader(http.StatusCreated)
w.Write([]byte("recurso criado"))

// fmt.Fprintf - formatação flexível
fmt.Fprintf(w, "Usuário %s criado com ID %d", name, id)

5. Gerenciamento de tempo e contextos

O context.Context permite propagar cancelamentos e deadlines através da cadeia de handlers:

func longRunningHandler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    select {
    case <-time.After(5 * time.Second):
        w.Write([]byte("Operação concluída"))
    case <-ctx.Done():
        http.Error(w, "Requisição cancelada", http.StatusRequestTimeout)
    }
}

Configurar timeouts no servidor é essencial para produção:

srv := &http.Server{
    Addr:         ":8080",
    ReadTimeout:  10 * time.Second,
    WriteTimeout: 15 * time.Second,
    IdleTimeout:  60 * time.Second,
}

Para graceful shutdown:

srv := &http.Server{Addr: ":8080"}
go srv.ListenAndServe()

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

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
srv.Shutdown(ctx)

6. Servindo conteúdo estático e arquivos

O http.FileServer serve arquivos de um diretório:

fs := http.FileServer(http.Dir("./static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))

http.StripPrefix remove o prefixo da URL antes de passar para o FileServer. Para usar arquivos embutidos (Go 1.16+):

//go:embed static/*
var staticFiles embed.FS

func main() {
    fs := http.FileServer(http.FS(staticFiles))
    http.Handle("/", fs)
    http.ListenAndServe(":8080", nil)
}

7. Tratamento de erros e respostas customizadas

Padrões para retornar códigos de status:

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    user, err := findUser(r.PathValue("id"))
    if err == sql.ErrNoRows {
        http.Error(w, `{"error":"usuário não encontrado"}`, http.StatusNotFound)
        return
    }
    if err != nil {
        http.Error(w, `{"error":"erro interno"}`, http.StatusInternalServerError)
        return
    }
    json.NewEncoder(w).Encode(user)
}

Helpers para respostas JSON:

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

func writeError(w http.ResponseWriter, status int, message string) {
    writeJSON(w, status, map[string]string{"error": message})
}

Middleware de panic recovery:

func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic: %v", err)
                http.Error(w, "Erro interno do servidor", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

8. Boas práticas e performance

Reutilização de buffers com sync.Pool:

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func handler(w http.ResponseWriter, r *http.Request) {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer bufferPool.Put(buf)
    buf.Reset()
    // usa buf para processar a requisição
}

Limitação de corpo de requisição:

func uploadHandler(w http.ResponseWriter, r *http.Request) {
    r.Body = http.MaxBytesReader(w, r.Body, 10<<20) // 10 MB
    // processa o upload
}

Testando handlers com httptest:

func TestHelloHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/hello", nil)
    rec := httptest.NewRecorder()

    helloHandler(rec, req)

    if rec.Code != http.StatusOK {
        t.Errorf("esperado 200, obtido %d", rec.Code)
    }
    if rec.Body.String() != "Hello!" {
        t.Errorf("esperado 'Hello!', obtido '%s'", rec.Body.String())
    }
}

O pacote net/http é poderoso e completo, permitindo construir desde APIs simples até servidores de alta performance sem dependências externas. Dominar seus recursos é fundamental para qualquer desenvolvedor Go.

Referências