Go: Serviços HTTP do Go

Última atualização: 2026-08-26

O pacote net/http da biblioteca padrão do Go é completo, permitindo que você desenvolva serviços web de nível de produção sem a necessidade de frameworks de terceiros.

Quando sua equipe decide “usar apenas as bibliotecas padrão, sem introduzir nenhum framework web”, você consegue escrever rotas e middleware de forma clara, assim como no Gin ou no Echo? Nesta lição, você vai dominar todas as tecnologias essenciais dos serviços HTTP em Go.

1. Você aprenderá


2. A história real de um engenheiro de backend

(1) Desafio: Uma API simples — escolhemos o Gin, mas, três meses depois, enfrentamos um obstáculo durante a atualização

Alice faz parte da equipe de back-end e precisa configurar uma API REST para o gerenciamento de usuários:

“Escrevi três rotas usando a estrutura Gin: GET /users, POST /users e GET /users/:id. Mas, três meses depois, o Go 1.22 foi lançado, e a biblioteca padrão passou a oferecer suporte nativo a parâmetros de método e de caminho. Agora, quero remover a dependência do Gin, mas teria que alterar todas as assinaturas dos manipuladores — gin.Context versus http.ResponseWriter. Meu chefe disse: ‘Não vale a pena refatorar centenas de linhas de código só para eliminar uma dependência.’”

A decisão dela na época:

GO
// Gin dependency version (wanted to migrate three months later)
r := gin.Default()
r.GET("/users", listUsers)               // gin.Context
r.POST("/users", createUser)             // gin.Context
r.GET("/users/:id", getUser)             // gin.Context
// Want to migrate to the standard library? All handler signatures must change!

(2) Solução para o Go 1.22: roteamento nativo na biblioteca padrão

GO
// Standard library version (Go 1.22+, no dependencies required)
package main

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

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

var users = []User{{ID: 1, Name: "Alice"}}

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

    // Go 1.22 enhanced routing: method + path pattern + path parameters
    mux.HandleFunc("GET /users", listUsers)
    mux.HandleFunc("POST /users", createUser)
    mux.HandleFunc("GET /users/{id}", getUser)

    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

// Standard handler signature: http.ResponseWriter + *http.Request
func listUsers(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

func createUser(w http.ResponseWriter, r *http.Request) {
    var u User
    if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    u.ID = len(users) + 1
    users = append(users, u)
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(u)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id") // Path parameter!
    // Look up user...
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(User{ID: 1, Name: "Alice"})
}

(3) Desempenho: Gin Style x Biblioteca Padrão (Go 1.22)

Recurso Gin (de terceiros) Biblioteca Padrão (Go < 1.22) Biblioteca Padrão (Go 1.22+)
Parâmetro de caminho :id ❌ Deve ser analisado manualmente {id}
Roteamento de método ❌ Verificar no manipulador "GET /path"
Resposta JSON c.JSON() Definir cabeçalho manualmente Definir cabeçalho manualmente
Dependências 1 pacote externo 0 0
Desempenho Um pouco mais lento (reflexão) Nativo Nativo
💡 Dica: O roteamento aprimorado do net/http do Go 1.22 é suficiente para a maioria dos projetos web. Se você não precisar de recursos específicos de frameworks (como vinculação/validação automática ou um ecossistema rico de middleware), dê prioridade à biblioteca padrão.


3. Noções básicas sobre HTTP

(1) ▶ Exemplo: O serviço HTTP mais simples

GO
package main

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

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}

func main() {
    http.HandleFunc("/hello", helloHandler)
    log.Println("Server started on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}
▶ Experimente

Teste:

BASH
$ curl "http://localhost:8080/hello?name=Alice"
Hello, Alice!

(2) Tipos de núcleo

Tipo Descrição
http.ResponseWriter Interface para escrever respostas HTTP
*http.Request Solicitação HTTP, incluindo URL, cabeçalhos, corpo e formulário
http.Handler Interface: ServeHTTP(w, r)
http.HandlerFunc Adaptador de função: converte uma função comum em um Handler
http.ServeMux Multiplexador de rota

(3) Uma explicação detalhada da interface do manipulador

GO
package main

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

// Method 1: Implement the Handler interface
type Greeter struct {
    Greeting string
}

func (g *Greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "%s, %s!", g.Greeting, r.URL.Path[1:])
}

// Method 2: HandlerFunc adapter
func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Query().Get("name"))
}

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

    // Struct Handler
    mux.Handle("/greet", &Greeter{Greeting: "Welcome"})

    // Function Handler (HandlerFunc automatic conversion)
    mux.HandleFunc("/hello", helloHandler)

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

4. Go 1.22: Roteamento aprimorado

(1) ▶ Exemplo: Método + Padrão de caminho + Parâmetro de caminho

GO 📖 Somente leitura
package main

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

type Item struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

var items = []Item{
    {ID: 1, Name: "Laptop", Price: 999.99},
    {ID: 2, Name: "Mouse", Price: 29.99},
}

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

    // Go 1.22 method + path pattern
    mux.HandleFunc("GET /items", listItems)
    mux.HandleFunc("POST /items", createItem)
    mux.HandleFunc("GET /items/{id}", getItem)
    mux.HandleFunc("PUT /items/{id}", updateItem)
    mux.HandleFunc("DELETE /items/{id}", deleteItem)

    // Wildcard suffix: path prefix matching
    mux.HandleFunc("GET /items/{path...}", wildcardHandler)

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

func listItems(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(items)
}

func createItem(w http.ResponseWriter, r *http.Request) {
    var item Item
    if err := json.NewDecoder(r.Body).Decode(&item); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    item.ID = len(items) + 1
    items = append(items, item)
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(item)
}

func getItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // Look up item...
    _ = id
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(items[0])
}

func updateItem(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    _ = id
    // Update logic...
    w.WriteHeader(http.StatusNoContent)
}

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

func wildcardHandler(w http.ResponseWriter, r *http.Request) {
    path := r.PathValue("path")
    w.Header().Set("Content-Type", "text/plain")
    http.Error(w, "Not found: "+path, http.StatusNotFound)
}
61 linhas de lógica (limite de 40, somente leitura)

(2) Comparação dos modos de roteamento no Go 1.22

Modo Go < 1.22 Go 1.22+ Exemplo
Correspondência de métodos Não suportado (instrução if dentro do Handler) Suportado "GET /items"
Parâmetro de caminho Não suportado {name} Sintaxe "GET /items/{id}"
Sufixo curinga Não suportado {path...} "GET /static/{file...}"
Caminho exato /items "GET /items" Correspondência exata para /items
Correspondência de prefixo /items/ "GET /items/" Corresponde a /items/...

(3) ▶ Exemplo: Prioridade de rota

GO
package main

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

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

    // Exact path > prefix path
    mux.HandleFunc("GET /items", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "items list")
    })
    mux.HandleFunc("GET /items/{id}", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "item %s\n", r.PathValue("id"))
    })
    mux.HandleFunc("GET /items/featured", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "featured items")
    })

    log.Print(http.ListenAndServe(":8080", mux))
}
▶ Experimente

Teste:

BASH
$ curl localhost:8080/item
item list
$ curl localhost:8080/item/42
item 42
$ curl localhost:8080/item/featured
featured item    # Exact match takes priority over {id} wildcard
🔥 Erro comum: A ordem em que as rotas são registradas não importa — a correspondência de rotas no Go 1.22 se baseia em regras de prioridade (exata > prefixo > curinga), e não na ordem de registro. As rotas com prioridade mais alta substituirão as correspondências com prioridade mais baixa, mesmo que estas últimas tenham sido registradas primeiro.


5. Solicitações e respostas

(1) ▶ Exemplo: Parâmetros de consulta, formulários, JSON

GO 📖 Somente leitura
package main

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

type Response struct {
    Method string      `json:"method"`
    Path   string      `json:"path"`
    Query  interface{} `json:"query,omitempty"`
    Form   interface{} `json:"form,omitempty"`
    JSON   interface{} `json:"json,omitempty"`
}

func handler(w http.ResponseWriter, r *http.Request) {
    resp := Response{
        Method: r.Method,
        Path:   r.URL.Path,
    }

    // Query parameters
    if r.Method == http.MethodGet {
        resp.Query = r.URL.Query()
    }

    // Form data
    if r.Method == http.MethodPost {
        contentType := r.Header.Get("Content-Type")
        switch {
        case contentType == "application/x-www-form-urlencoded":
            r.ParseForm()
            resp.Form = r.Form
        case contentType == "application/json":
            var body interface{}
            json.NewDecoder(r.Body).Decode(&body)
            resp.JSON = body
        }
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", handler)

    log.Print(http.ListenAndServe(":8080", mux))
}
42 linhas de lógica (limite de 40, somente leitura)

(2) ▶ Exemplo: Função utilitária de resposta JSON

GO
package main

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

// JSON response utility function
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})
}

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func getProduct(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // Simulate lookup
    if id != "1" {
        writeError(w, http.StatusNotFound, "product not found")
        return
    }
    writeJSON(w, http.StatusOK, Product{ID: 1, Name: "Laptop", Price: 999.99})
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /products/{id}", getProduct)

    log.Print(http.ListenAndServe(":8080", mux))
}
▶ Experimente
100%
sequenceDiagram
    participant Client as HTTP Client
    participant Mux as ServeMux
    participant Handler as Handler
    
    Client->>Mux: GET /products/1
    Mux->>Mux: Route matching
    Mux->>Handler: ServeHTTP(w, r)
    Handler->>Handler: r.PathValue("id") → "1"
    Handler->>Handler: writeJSON(w, 200, product)
    Handler-->>Client: HTTP 200 + JSON body

(3) Referência rápida aos códigos de status HTTP

Código Constante Finalidade
200 http.StatusOK Sucesso
201 http.StatusCreated Recurso criado com sucesso
204 http.StatusNoContent Sucesso, mas sem corpo da resposta
400 http.StatusBadRequest Erro na solicitação do cliente
401 http.StatusUnauthorized Não autorizado
403 http.StatusForbidden Sem permissão
404 http.StatusNotFound Recurso não encontrado
500 http.StatusInternalServerError Erro interno do servidor

6. Serviço de arquivos estáticos

(1) ▶ Exemplo: Arquivos estáticos

GO
package main

import (
    "log"
    "net/http"
)

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

    // Static file service: /static/ prefix → ./static/ directory
    mux.Handle("GET /static/", http.StripPrefix("/static/",
        http.FileServer(http.Dir("./static"))))

    // Single file
    mux.Handle("GET /favicon.ico", http.FileServer(http.Dir("./static")))

    log.Print(http.ListenAndServe(":8080", mux))
}
▶ Experimente

7. Exemplo completo: API de notas no estilo REST

GO
// notes_api.go
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "sync"
    "time"
)

// ---------- Model ----------

type Note struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Content   string    `json:"content"`
    CreatedAt time.Time `json:"created_at"`
    UpdatedAt time.Time `json:"updated_at"`
}

// ---------- Store ----------

type NoteStore struct {
    mu     sync.RWMutex
    notes  map[int]Note
    nextID int
}

func NewNoteStore() *NoteStore {
    return &NoteStore{
        notes:  make(map[int]Note),
        nextID: 1,
    }
}

func (s *NoteStore) Create(title, content string) Note {
    s.mu.Lock()
    defer s.mu.Unlock()
    n := Note{
        ID:        s.nextID,
        Title:     title,
        Content:   content,
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }
    s.nextID++
    s.notes[n.ID] = n
    return n
}

func (s *NoteStore) List() []Note {
    s.mu.RLock()
    defer s.mu.RUnlock()
    result := make([]Note, 0, len(s.notes))
    for _, n := range s.notes {
        result = append(result, n)
    }
    return result
}

func (s *NoteStore) Get(id int) (Note, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    n, ok := s.notes[id]
    return n, ok
}

func (s *NoteStore) Update(id int, title, content string) (Note, bool) {
    s.mu.Lock()
    defer s.mu.Unlock()
    n, ok := s.notes[id]
    if !ok {
        return Note{}, false
    }
    n.Title = title
    n.Content = content
    n.UpdatedAt = time.Now()
    s.notes[id] = n
    return n, true
}

func (s *NoteStore) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    _, ok := s.notes[id]
    if !ok {
        return false
    }
    delete(s.notes, id)
    return true
}

// ---------- API ----------

type NotesAPI struct {
    store *NoteStore
}

func NewNotesAPI(store *NoteStore) *NotesAPI {
    return &NotesAPI{store: store}
}

func (api *NotesAPI) Register(mux *http.ServeMux) {
    mux.HandleFunc("GET /notes", api.ListNotes)
    mux.HandleFunc("POST /notes", api.CreateNote)
    mux.HandleFunc("GET /notes/{id}", api.GetNote)
    mux.HandleFunc("PUT /notes/{id}", api.UpdateNote)
    mux.HandleFunc("DELETE /notes/{id}", api.DeleteNote)
}

// Utility functions
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})
}

// ---------- Handlers ----------

func (api *NotesAPI) ListNotes(w http.ResponseWriter, r *http.Request) {
    notes := api.store.List()
    writeJSON(w, http.StatusOK, notes)
}

func (api *NotesAPI) CreateNote(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Title   string `json:"title"`
        Content string `json:"content"`
    }
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }
    if input.Title == "" {
        writeError(w, http.StatusBadRequest, "title is required")
        return
    }

    note := api.store.Create(input.Title, input.Content)
    writeJSON(w, http.StatusCreated, note)
}

func (api *NotesAPI) GetNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    note, ok := api.store.Get(id)
    if !ok {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    writeJSON(w, http.StatusOK, note)
}

func (api *NotesAPI) UpdateNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    var input struct {
        Title   string `json:"title"`
        Content string `json:"content"`
    }
    if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON body")
        return
    }

    note, ok := api.store.Update(id, input.Title, input.Content)
    if !ok {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    writeJSON(w, http.StatusOK, note)
}

func (api *NotesAPI) DeleteNote(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid note ID")
        return
    }

    if !api.store.Delete(id) {
        writeError(w, http.StatusNotFound, "note not found")
        return
    }
    w.WriteHeader(http.StatusNoContent)
}

func main() {
    store := NewNoteStore()
    api := NewNotesAPI(store)

    mux := http.NewServeMux()
    api.Register(mux)

    log.Println("Note API started on :8080")
    log.Println("Available endpoints:")
    log.Println("  GET    /notes       — List all notes")
    log.Println("  POST   /notes       — Create a note")
    log.Println("  GET    /notes/{id}  — Get a single note")
    log.Println("  PUT    /notes/{id}  — Update a note")
    log.Println("  DELETE /notes/{id}  — Delete a note")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
🔥 Erro comum: http.Error(w, msg, code) não define Content-Type como JSON. Se você estiver retornando um erro JSON, use json.NewEncoder(w).Encode(errResp) e defina manualmente Header(). O http.Error() da biblioteca padrão retorna texto simples.


❓ Perguntas Frequentes

P: Como as melhorias de roteamento no Go 1.22 se comparam ao Gin? R: Elas são suficientes para a maioria dos cenários. O Go 1.22 oferece suporte à correspondência de métodos, parâmetros de caminho e curingas. O valor agregado do Gin está na vinculação/validação de solicitações, em seu ecossistema de middleware e no tratamento de erros. Se o seu projeto não exigir esses recursos, a biblioteca padrão é mais leve e mais segura (sem dependências).

P: Qual é a diferença entre Handler e HandlerFunc? R: Handler é uma interface (que requer a implementação do método ServeHTTP), enquanto HandlerFunc é um adaptador de tipo de função — ele permite que funções comuns satisfaçam automaticamente a interface Handler. Os dois são completamente equivalentes: mux.Handle("/path", handler) e mux.HandleFunc("/path", handlerFunc) têm o mesmo efeito.

P: Como faço para recuperar parâmetros de caminho? R: No Go 1.22 e versões posteriores, use r.PathValue("name") para recuperar parâmetros de caminho do tipo {name}. Em versões anteriores, é necessário analisá-los manualmente a partir de r.URL.Path ou usar uma biblioteca de terceiros. Os nomes dos parâmetros de caminho devem corresponder aos {name} no padrão de rota.

P: Qual é a diferença entre ListenAndServe e ListenAndServeTLS? R: O primeiro usa HTTP (porta 80), enquanto o segundo usa HTTPS (porta 443) e requer um certificado e um arquivo de chave privada. Exemplo de HTTPS: http.ListenAndServeTLS(":443", "cert.pem", "key.pem", mux).

P: Como faço para desligar um servidor HTTP de maneira controlada? R: Use server.Shutdown(ctx) em conjunto com os/signal para detectar sinais de encerramento. Shutdown aguardará até que todas as conexões ativas sejam processadas antes de fechar. Não use server.Close() — ele encerrará à força as solicitações que estiverem sendo processadas no momento.

P: O que significa passar nil como o segundo 引数 para http.ListenAndServe? R: Passar nil indica que http.DefaultServeMux (o roteador padrão global) é utilizado. Recomenda-se criar explicitamente http.NewServeMux() para evitar interferir no roteamento global — especialmente ao isolar rotas durante os testes.

P: O ServeMux suporta subrotas aninhadas? R: O ServeMux da biblioteca padrão não suporta rotas aninhadas (agrupamento de subrotas). É possível obter um efeito semelhante registrando manualmente as rotas com um prefixo comum ou utilizando uma biblioteca de roteamento de terceiros, como o chi (que é extremamente leve), para implementar o aninhamento.


📖 Resumo


📝 Exercícios

  1. Exercício básico (Dificuldade ⭐): Crie um serviço HTTP simples e registre três rotas: GET /time retorna a hora atual em JSON, GET /health retorna {"status": "ok"} e GET /version retorna o número da versão. Use a sintaxe de roteamento aprimorada do Go 1.22.

  2. Problema avançado (Dificuldade ⭐⭐): Implemente uma API de lista de tarefas. Requisitos: (1) Operações CRUD completas; (2) Use métodos do Go 1.22 e roteamento baseado em caminho; (3) Solicitações e respostas em JSON; (4) Armazenamento em memória (map + proteção por RWMutex); (5) Retorne códigos de status HTTP apropriados.

  3. Desafio (Dificuldade: ⭐⭐⭐): Implemente um serviço de encurtamento de URL. Requisitos: (1) POST /shorten aceita uma URL longa e retorna um código curto (uma sequência aleatória de 6 caracteres); (2) GET /{code} deve realizar um redirecionamento 301 para a URL original; (3) Estatísticas de acesso: GET /stats/{code} deve retornar o número de visitas; (4) Use -race para verificar a segurança de concorrência; (5) Use um RWMutex para proteger o contador de estatísticas.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%