Go Admin

Pacote net/http em Go: Servidor e Cliente HTTP da Stdlib na Prática Já leu

Introdução ao Pacote net/http de Go O pacote é uma das bibliotecas padrão mais poderosas do Go, oferecendo suporte completo para construir servidores e clientes HTTP sem dependências externas. Diferentemente de outras linguagens que frequentemente dependem de frameworks pesados, Go fornece primitivas suficientemente baixo nível e bem projetadas para que você possa criar aplicações HTTP escaláveis e eficientes diretamente da stdlib. A filosofia do Go nesse aspecto é clara: forneça ferramentas robustas, simples e compostas. O pacote segue rigorosamente esse princípio. Neste artigo, você compreenderá como os componentes principais funcionam, desde a anatomia de um servidor HTTP até a execução de requisições de cliente, passando por conceitos como handlers, middlewares e tratamento de erros. Fundamentos do Servidor HTTP Conceito e Arquitetura Básica Um servidor HTTP em Go é construído sobre o tipo , que encapsula toda a lógica necessária para escutar conexões TCP, interpretar requisições HTTP e enviar respostas. A forma mais simples de iniciar um servidor é usando ,

Introdução ao Pacote net/http de Go

O pacote net/http é uma das bibliotecas padrão mais poderosas do Go, oferecendo suporte completo para construir servidores e clientes HTTP sem dependências externas. Diferentemente de outras linguagens que frequentemente dependem de frameworks pesados, Go fornece primitivas suficientemente baixo nível e bem projetadas para que você possa criar aplicações HTTP escaláveis e eficientes diretamente da stdlib.

A filosofia do Go nesse aspecto é clara: forneça ferramentas robustas, simples e compostas. O pacote net/http segue rigorosamente esse princípio. Neste artigo, você compreenderá como os componentes principais funcionam, desde a anatomia de um servidor HTTP até a execução de requisições de cliente, passando por conceitos como handlers, middlewares e tratamento de erros.

Fundamentos do Servidor HTTP

Conceito e Arquitetura Básica

Um servidor HTTP em Go é construído sobre o tipo http.Server, que encapsula toda a lógica necessária para escutar conexões TCP, interpretar requisições HTTP e enviar respostas. A forma mais simples de iniciar um servidor é usando http.ListenAndServe(), que combina criação, configuração e escuta em uma única chamada. Por baixo dos panos, Go cria uma goroutine para cada conexão cliente, permitindo que centenas de milhares de requisições simultâneas sejam processadas de forma eficiente.

O conceito fundamental é o http.Handler. Trata-se de uma interface simples com apenas um método: ServeHTTP(ResponseWriter, *Request). Qualquer tipo que implemente esse método pode processar requisições HTTP. Essa simplicidade é intencional: permite composição, reutilização e testes diretos sem abstração desnecessária.

package main

import (
    "fmt"
    "net/http"
)

// Handler simples que implementa a interface http.Handler
type HelloHandler struct{}

func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    fmt.Fprintf(w, "Olá, %s!", r.URL.Query().Get("nome"))
}

func main() {
    handler := &HelloHandler{}
    http.ListenAndServe(":8080", handler)
}

Funções Convenientes e Roteamento Básico

Para a maioria dos casos reais, você não criará tipos personalizados que implementam http.Handler. Em vez disso, usará http.HandleFunc(), que converte uma função simples em um handler. Essa função recebe os mesmos parâmetros que ServeHTTP() e é transformada internamente em um tipo que implementa a interface.

package main

import (
    "fmt"
    "net/http"
)

func main() {
    // Registra handlers diretamente com funções
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        fmt.Fprintf(w, `{"mensagem":"Página inicial"}`)
    })

    http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
            http.Error(w, "Método não permitido", http.StatusMethodNotAllowed)
            return
        }
        fmt.Fprintf(w, `{"usuarios":["Alice","Bob"]}`)
    })

    fmt.Println("Servidor rodando em :8080")
    http.ListenAndServe(":8080", nil)
}

Quando você passa nil como segundo argumento de ListenAndServe(), Go usa o DefaultServeMux, um multiplexador global que armazena todos os handlers registrados via http.HandleFunc() e http.Handle(). Para aplicações maiores, crie um ServeMux personalizado para evitar efeitos colaterais globais.

Criando Middlewares Robustos

Middlewares são funções que envolvem handlers, adicionando comportamentos como logging, autenticação, CORS ou compressão. A forma idiomática em Go é usar uma função que recebe um http.Handler e retorna outro http.Handler.

package main

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

// Middleware de logging que registra informações sobre cada requisição
func logMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        inicio := time.Now()
        log.Printf("[%s] %s %s", r.Method, r.RequestURI, r.RemoteAddr)

        next.ServeHTTP(w, r)

        duracao := time.Since(inicio)
        log.Printf("Requisição processada em %v", duracao)
    })
}

// Middleware de autenticação simples
func autenticacaoMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token != "Bearer secret-token-123" {
            http.Error(w, "Não autorizado", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

// Função auxiliar para encadear middlewares
func comMiddlewares(handler http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

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

    // Handler principal
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Dados sensíveis retornados com sucesso")
    })

    // Aplica middlewares na ordem desejada
    mux.Handle("/dados", comMiddlewares(
        handler,
        logMiddleware,
        autenticacaoMiddleware,
    ))

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

Cliente HTTP em Go

Fazendo Requisições Básicas

O pacote net/http fornece o tipo http.Client, que representa um cliente HTTP reutilizável. A abordagem padrão é criar um cliente uma única vez e reutilizá-lo para múltiplas requisições, pois ele gerencia pool de conexões internamente, economizando recursos e melhorando performance.

package main

import (
    "fmt"
    "io"
    "net/http"
)

func main() {
    // Criar um cliente HTTP
    client := &http.Client{}

    // Fazer uma requisição GET simples
    resp, err := client.Get("https://api.github.com/users/golang")
    if err != nil {
        fmt.Printf("Erro na requisição: %v\n", err)
        return
    }
    defer resp.Body.Close()

    // Ler o corpo da resposta
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Erro ao ler resposta: %v\n", err)
        return
    }

    fmt.Printf("Status: %d\n", resp.StatusCode)
    fmt.Printf("Headers: %v\n", resp.Header)
    fmt.Printf("Body: %s\n", string(body))
}

Requisições Customizadas com http.Request

Para casos onde você precisa controlar detalhes como headers personalizados, método HTTP específico ou timeout, construa uma requisição manualmente usando http.NewRequest(). Isso oferece granularidade completa sobre o que é enviado ao servidor.

package main

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

func main() {
    // Dados que serão enviados no corpo da requisição
    dados := map[string]interface{}{
        "titulo": "Novo Post",
        "corpo":  "Este é um exemplo de POST com JSON",
    }

    // Serializar para JSON
    payload, _ := json.Marshal(dados)

    // Criar requisição manualmente
    req, err := http.NewRequest(
        http.MethodPost,
        "https://api.exemplo.com/posts",
        bytes.NewBuffer(payload),
    )
    if err != nil {
        fmt.Printf("Erro ao criar requisição: %v\n", err)
        return
    }

    // Adicionar headers customizados
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer token-123")
    req.Header.Set("User-Agent", "MeuApp/1.0")

    // Executar a requisição
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("Erro ao executar requisição: %v\n", err)
        return
    }
    defer resp.Body.Close()

    fmt.Printf("Status: %d\n", resp.StatusCode)
}

Tratamento de Timeouts e Resiliência

Em ambientes de produção, timeouts são essenciais para evitar que seu programa fica preso esperando por servidores lentos ou não responsivos. Configure timeouts no http.Client usando time.Duration.

package main

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

func main() {
    // Cliente com timeouts configurados
    client := &http.Client{
        Timeout: 5 * time.Second,
    }

    // Você também pode configurar timeouts específicos
    client.Timeout = 0 // Desabilita timeout global
    transport := &http.Transport{
        DialTimeout:         3 * time.Second,      // Timeout para conectar
        TLSHandshakeTimeout: 2 * time.Second,      // Timeout para handshake TLS
        IdleConnTimeout:     30 * time.Second,     // Timeout de inatividade
    }
    client.Transport = transport

    // Usar o cliente com retry simples
    maxRetries := 3
    var resp *http.Response
    var err error

    for tentativa := 1; tentativa <= maxRetries; tentativa++ {
        resp, err = client.Get("https://httpbin.org/delay/2")
        if err == nil {
            break
        }
        fmt.Printf("Tentativa %d falhou: %v\n", tentativa, err)
        if tentativa < maxRetries {
            time.Sleep(time.Second * time.Duration(tentativa))
        }
    }

    if err != nil {
        fmt.Printf("Todas as tentativas falharam: %v\n", err)
        return
    }

    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    fmt.Printf("Resposta recebida: %d bytes\n", len(body))
}

Padrões Avançados e Boas Práticas

Context para Cancelamento e Deadlines

O pacote context é fundamental para gerenciar ciclos de vida de requisições em Go. Use-o para implementar cancelamento, deadlines e passar valores através da cadeia de funções de forma segura e explícita.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "time"
)

func main() {
    // Criar um contexto com deadline
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    // Criar requisição associada ao contexto
    req, _ := http.NewRequestWithContext(
        ctx,
        http.MethodGet,
        "https://httpbin.org/delay/5",
        nil,
    )

    // Executar requisição
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Printf("Erro: %v\n", err) // Timeout será respeitado
        return
    }
    defer resp.Body.Close()

    io.ReadAll(resp.Body)
    fmt.Println("Sucesso")
}

No lado do servidor, você também pode usar context para passar valores entre middlewares:

package main

import (
    "context"
    "fmt"
    "net/http"
)

type ContextKey string

const UserIDKey ContextKey = "user_id"

// Middleware que extrai user_id do header e armazena no context
func extrairUserMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := r.Header.Get("X-User-ID")
        ctx := context.WithValue(r.Context(), UserIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

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

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Recuperar user_id do context
        userID := r.Context().Value(UserIDKey).(string)
        fmt.Fprintf(w, "User ID: %s", userID)
    })

    mux.Handle("/perfil", extrairUserMiddleware(handler))
    http.ListenAndServe(":8080", mux)
}

Error Handling e Status Codes Apropriados

A função http.Error() é conveniente, mas em aplicações reais você frequentemente quer retornar JSON estruturado. Crie helpers que padronizem suas respostas de erro.

package main

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

// Estrutura padrão para respostas de erro
type ErrorResponse struct {
    Erro   string `json:"erro"`
    Codigo int    `json:"codigo"`
    Detalhe string `json:"detalhe,omitempty"`
}

// Helper para retornar erros em JSON
func responderErro(w http.ResponseWriter, statusCode int, mensagem string, detalhe string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(statusCode)

    resposta := ErrorResponse{
        Erro:    mensagem,
        Codigo:  statusCode,
        Detalhe: detalhe,
    }

    json.NewEncoder(w).Encode(resposta)
}

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

    mux.HandleFunc("/recurso", func(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
            responderErro(w, http.StatusMethodNotAllowed, "Método não permitido", "Apenas GET é aceito")
            return
        }

        id := r.URL.Query().Get("id")
        if id == "" {
            responderErro(w, http.StatusBadRequest, "Parâmetro ausente", "Parâmetro 'id' é obrigatório")
            return
        }

        // Simular busca de recurso
        if id != "123" {
            responderErro(w, http.StatusNotFound, "Recurso não encontrado", fmt.Sprintf("ID %s não existe", id))
            return
        }

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{"id": id, "nome": "Recurso"})
    })

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

Conclusão

Durante este artigo, você aprendeu três conceitos fundamentais que formam a base para trabalhar com HTTP em Go. Primeiro, compreendeu que um servidor HTTP em Go é construído sobre a elegante interface http.Handler, permitindo composição simples através de middlewares sem frameworks pesados. Segundo, viu que o cliente HTTP (http.Client) deve ser reutilizado e configurado com devida atenção a timeouts e resiliência. Terceiro, internalizou que padrões idiomáticos como context para cancelamento e estruturas de erro padronizadas não são luxos, mas necessidades reais em código de produção.

O pacote net/http de Go é deliberadamente minimalista, mas essa aparente simplicidade mascara um design profundo. Use-o sem medo de que você está reinventando a roda — o Go fez esse trabalho fundamental tão bem que raramente você precisará de dependências externas para HTTP. A chave está em entender seus primitivos e aprender a combiná-los efetivamente.

Referências


Artigos relacionados