Vamos construir uma API REST funcional para gerenciar uma lista de tarefas (TO-DO). Esse é o "hello world" de backend.
O que vamos construir
Uma API com 4 endpoints:
| Método | Rota | O que faz |
|---|---|---|
GET | /tarefas | Lista todas as tarefas |
GET | /tarefas/{id} | Busca uma tarefa específica |
POST | /tarefas | Cria uma nova tarefa |
DELETE | /tarefas/{id} | Remove uma tarefa |
Estrutura do projeto
api-tarefas/
├── go.mod
└── main.go
Para começar, num terminal:
bash
mkdir api-tarefas && cd api-tarefas
go mod init github.com/seu-usuario/api-tarefasO que
go mod init faz? Cria o go.mod, que é como o package.json do Node ou o pom.xml do Maven. Ele rastreia dependências e a versão do Go.O código completo (main.go)
go
package main
import (
"encoding/json" // serialização JSON
"fmt"
"log"
"net/http" // servidor HTTP da stdlib
"strconv"
"sync" // para sincronização (mutex)
)
// ==========================================================
// MODELO: como uma tarefa se parece
// ==========================================================
type Task struct {
// As tags `json:"..."` definem como o campo aparece em JSON
ID int `json:"id"`
Title string `json:"titulo"`
Done bool `json:"concluida"`
}
// ==========================================================
// REPOSITÓRIO: onde guardamos os dados
// (Em produção seria um banco de dados; aqui usamos memória)
// ==========================================================
type Repository struct {
mu sync.RWMutex // protege contra acessos concorrentes
tasks map[int]Task
nextID int
}
func NewRepository() *Repository {
return &Repository{
tasks: make(map[int]Task),
nextID: 1,
}
}
// Listar retorna todas as tarefas (cópia segura).
// RLock = lock de LEITURA: vários podem ler ao mesmo tempo.
func (r *Repository) Listar() []Task {
r.mu.RLock()
defer r.mu.RUnlock()
lista := make([]Task, 0, len(r.tasks))
for _, t := range r.tasks {
lista = append(lista, t)
}
return lista
}
// Buscar pega uma tarefa pelo ID.
// Retorna (tarefa, true) se achou, (vazia, false) se não.
func (r *Repository) Buscar(id int) (Task, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
t, ok := r.tasks[id]
return t, ok
}
// Criar adiciona uma nova tarefa e retorna ela com ID.
// Lock = lock de ESCRITA: só um por vez.
func (r *Repository) Criar(title string) Task {
r.mu.Lock()
defer r.mu.Unlock()
t := Task{
ID: r.nextID,
Title: title,
Done: false,
}
r.tasks[t.ID] = t
r.nextID++
return t
}
// Remover apaga pelo ID. Retorna true se removeu, false se não existia.
func (r *Repository) Remover(id int) bool {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.tasks[id]; !exists {
return false
}
delete(r.tasks, id)
return true
}
// ==========================================================
// HANDLERS: as funções que recebem requisições HTTP
// ==========================================================
type Handler struct {
repo *Repository
}
// responderJSON é uma função utilitária que padroniza respostas JSON
func responderJSON(w http.ResponseWriter, status int, dados any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if dados != nil {
if err := json.NewEncoder(w).Encode(dados); err != nil {
log.Println("erro ao codificar JSON:", err)
}
}
}
// responderErro padroniza mensagens de erro
func responderErro(w http.ResponseWriter, status int, mensagem string) {
responderJSON(w, status, map[string]string{"erro": mensagem})
}
// handleTasks roteia requisições para /tarefas (sem ID)
func (h *Handler) handleTasks(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// GET /tarefas → lista todas
responderJSON(w, http.StatusOK, h.repo.Listar())
case http.MethodPost:
// POST /tarefas → cria nova
var entrada struct {
Title string `json:"titulo"`
}
// Decodifica o JSON do body em entrada
if err := json.NewDecoder(r.Body).Decode(&entrada); err != nil {
responderErro(w, http.StatusBadRequest, "JSON inválido")
return
}
if entrada.Title == "" {
responderErro(w, http.StatusBadRequest, "título é obrigatório")
return
}
nova := h.repo.Criar(entrada.Title)
responderJSON(w, http.StatusCreated, nova)
default:
responderErro(w, http.StatusMethodNotAllowed, "método não permitido")
}
}
// handleTaskByID lida com /tarefas/{id}
func (h *Handler) handleTaskByID(w http.ResponseWriter, r *http.Request) {
// Extrai o ID da URL: /tarefas/42 → "42"
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
responderErro(w, http.StatusBadRequest, "ID inválido")
return
}
switch r.Method {
case http.MethodGet:
t, ok := h.repo.Buscar(id)
if !ok {
responderErro(w, http.StatusNotFound, "tarefa não encontrada")
return
}
responderJSON(w, http.StatusOK, t)
case http.MethodDelete:
if !h.repo.Remover(id) {
responderErro(w, http.StatusNotFound, "tarefa não encontrada")
return
}
responderJSON(w, http.StatusNoContent, nil)
default:
responderErro(w, http.StatusMethodNotAllowed, "método não permitido")
}
}
// ==========================================================
// MAIN: monta tudo e sobe o servidor
// ==========================================================
func main() {
repo := NewRepository()
handler := &Handler{repo: repo}
// O ServeMux do Go 1.22+ suporta padrões avançados como {id}
mux := http.NewServeMux()
mux.HandleFunc("/tarefas", handler.handleTasks)
mux.HandleFunc("/tarefas/{id}", handler.handleTaskByID)
porta := ":8080"
fmt.Printf("Servidor rodando em http://localhost%s\n", porta)
// ListenAndServe BLOQUEIA — só retorna se der erro fatal
if err := http.ListenAndServe(porta, mux); err != nil {
log.Fatal("servidor falhou:", err)
}
}Testando a API
Rode com:
bash
go run main.goEm outro terminal:
bash
# Criar uma tarefa
curl -X POST http://localhost:8080/tarefas \
-H "Content-Type: application/json" \
-d '{"titulo": "Estudar Go"}'
# Listar tarefas
curl http://localhost:8080/tarefas
# Buscar tarefa específica
curl http://localhost:8080/tarefas/1
# Remover
curl -X DELETE http://localhost:8080/tarefas/1Por que tomamos cada decisão?
| Decisão | Motivo |
|---|---|
| Sem framework (Gin, Echo) | A net/http da stdlib é poderosíssima. Para aprender, melhor não esconder atrás de mágica. |
| Mutex no repositório | Servidores HTTP em Go atendem várias requisições concorrentemente. Sem lock, dois clientes podem corromper o map. |
RWMutex em vez de Mutex | Permite várias leituras simultâneas, só bloqueando escritas. Ganho enorme em APIs read-heavy. |
| Camadas (handler/repositório) | Separar responsabilidades facilita testes e troca de banco depois. |
Funções utilitárias responderJSON | Evita repetição de w.Header().Set(...) em todo lugar. |
json.NewDecoder(r.Body) | Mais eficiente que ler tudo e dar Unmarshal — funciona como stream. |
| Status HTTP corretos | 201 Created para criação, 204 No Content para deletar, 404 para não encontrado. RESTful de verdade. |