Nesta aula, vamos construir uma API REST completa em Go, aplicando os conceitos de roteamento, banco de dados, serialização JSON e middleware. Você aprenderá a criar endpoints CRUD (Create, Read, Update, Delete), conectar-se a um banco de dados SQL, lidar com requisições e respostas JSON, e implementar middlewares para autenticação e logging. Ao final, você terá uma base sólida para desenvolver APIs profissionais em Go.

Vamos utilizar a biblioteca padrão do Go, especialmente os pacotes net/http, database/sql e encoding/json, além de um driver para SQLite (para simplicidade) ou MySQL. O foco é entender os fundamentos sem depender de frameworks pesados, embora você possa adaptar para gin ou echo depois.

Rotas CRUD

O primeiro passo é definir as rotas da nossa API. Em Go, o pacote net/http permite registrar handlers para diferentes métodos HTTP e caminhos. Vamos criar uma estrutura simples para gerenciar produtos, com endpoints para listar, obter por ID, criar, atualizar e deletar.

Definimos um tipo Product e um handler que despacha as requisições baseado no método HTTP. Para isso, usamos http.ServeMux ou criamos um handler manualmente. Vamos usar http.ServeMux com padrões que incluem o método, pois a partir do Go 1.22 é possível especificar o método no padrão, como "POST /products".

package main

import (
    "database/sql"
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "strings"

    _ "github.com/mattn/go-sqlite3"
)

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

var db *sql.DB

func main() {
    var err error
    db, err = sql.Open("sqlite3", "./products.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // Criar tabela se não existir
    createTable()

    mux := http.NewServeMux()
    mux.HandleFunc("GET /products", listProducts)
    mux.HandleFunc("GET /products/{id}", getProduct)
    mux.HandleFunc("POST /products", createProduct)
    mux.HandleFunc("PUT /products/{id}", updateProduct)
    mux.HandleFunc("DELETE /products/{id}", deleteProduct)

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

func createTable() {
    query := `CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        price REAL NOT NULL
    );`
    _, err := db.Exec(query)
    if err != nil {
        log.Fatal(err)
    }
}

Perceba que usamos o padrão "GET /products" etc. Isso exige Go 1.22 ou superior. Se você estiver usando uma versão anterior, pode usar mux.HandleFunc("/products", productsHandler) e verificar o método manualmente. Vamos agora implementar cada handler.

database/sql

O pacote database/sql fornece uma interface genérica para bancos de dados SQL. Precisamos de um driver específico (neste caso, SQLite). O padrão é abrir uma conexão, executar consultas e gerenciar erros.

Vamos implementar as funções que interagem com o banco. Para listar produtos, usamos db.Query e iteramos sobre as linhas. Para obter um produto específico, usamos QueryRow. Para criar, usamos Exec e recuperamos o último ID inserido. Para atualizar e deletar, usamos Exec.

func listProducts(w http.ResponseWriter, r *http.Request) {
    rows, err := db.Query("SELECT id, name, price FROM products")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    defer rows.Close()

    products := []Product{}
    for rows.Next() {
        var p Product
        if err := rows.Scan(&p.ID, &p.Name, &p.Price); err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            return
        }
        products = append(products, p)
    }
    if err := rows.Err(); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

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

func getProduct(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }

    var p Product
    err = db.QueryRow("SELECT id, name, price FROM products WHERE id = ?", id).Scan(&p.ID, &p.Name, &p.Price)
    if err == sql.ErrNoRows {
        http.NotFound(w, r)
        return
    } else if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

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

func createProduct(w http.ResponseWriter, r *http.Request) {
    var p Product
    if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }
    if p.Name == "" || p.Price <= 0 {
        http.Error(w, "Name and positive price required", http.StatusBadRequest)
        return
    }

    result, err := db.Exec("INSERT INTO products (name, price) VALUES (?, ?)", p.Name, p.Price)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    id, _ := result.LastInsertId()
    p.ID = int(id)

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(p)
}

func updateProduct(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }

    var p Product
    if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    result, err := db.Exec("UPDATE products SET name = ?, price = ? WHERE id = ?", p.Name, p.Price, id)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    rowsAffected, _ := result.RowsAffected()
    if rowsAffected == 0 {
        http.NotFound(w, r)
        return
    }

    p.ID = id
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(p)
}

func deleteProduct(w http.ResponseWriter, r *http.Request) {
    idStr := r.PathValue("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }

    result, err := db.Exec("DELETE FROM products WHERE id = ?", id)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    rowsAffected, _ := result.RowsAffected()
    if rowsAffected == 0 {
        http.NotFound(w, r)
        return
    }

    w.WriteHeader(http.StatusNoContent)
}

Note que usamos r.PathValue("id") para extrair o parâmetro da URL, disponível no Go 1.22. Para versões anteriores, você precisaria usar r.URL.Path e manipular strings. Além disso, sempre fechamos as linhas (rows.Close()) e verificamos erros.

JSON

O pacote encoding/json é essencial para serializar e desserializar dados. Usamos tags nas structs para controlar os nomes dos campos no JSON. Por exemplo, json:"id".

Para enviar respostas JSON, definimos o header Content-Type: application/json e usamos json.NewEncoder(w).Encode(). Para receber dados, usamos json.NewDecoder(r.Body).Decode(). É importante validar os dados recebidos antes de usar.

// Exemplo de struct com tags JSON

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

// Enviando resposta JSON
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(product)

// Decodificando corpo JSON
var p Product
if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
    http.Error(w, "Invalid JSON", http.StatusBadRequest)
    return
}

Lembre-se de que a decodificação não valida automaticamente se os campos estão presentes ou com tipos corretos. Você deve fazer isso manualmente. Por exemplo, se o campo name for omitido, ele será zero (""). Portanto, valide conforme necessário.

Middleware

Middleware são funções que envolvem os handlers para executar lógica antes ou depois deles. Em Go, um middleware é uma função que recebe um http.Handler e retorna outro http.Handler. Podemos usá-los para logging, autenticação, recuperação de pânico, etc.

Vamos implementar um middleware de logging que registra o método, caminho e duração da requisição, e um middleware de autenticação simples que verifica um token no header. Para aplicar os middlewares, usamos uma função que envolve o mux.

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("%s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        log.Printf("Completed in %v", time.Since(start))
    })
}

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token != "secret-token" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

func main() {
    // ... criação do mux e handlers

    // Aplicar middlewares
    handler := loggingMiddleware(authMiddleware(mux))

    log.Println("Server running on :8080")
    log.Fatal(http.ListenAndServe(":8080", handler))
}

No exemplo, o middleware de autenticação é aplicado a todas as rotas. Se quiser aplicar a apenas algumas, você pode criar wrappers individuais. Também é possível usar a técnica de cadeia de middlewares para combinar vários.

É importante notar que a ordem dos middlewares importa: o primeiro a ser aplicado é o mais externo, ou seja, executa primeiro. No exemplo, primeiro o logging, depois a autenticação.

Boas Práticas e Observações Finais

Ao construir APIs REST em Go, considere as seguintes boas práticas:

  • Use http.Error para respostas de erro, mas defina códigos de status apropriados (400, 404, 500, etc.).
  • Valide todas as entradas, tanto na URL quanto no corpo da requisição.
  • Use defer para fechar recursos como rows e db quando apropriado.
  • Estruture o código em pacotes (handlers, models, database) para melhor organização.
  • Considere usar contextos (context.Context) para cancelamento e deadlines.
  • Implemente middlewares para logging, recuperação de pânico (recover) e CORS, se necessário.
  • Teste sua API com ferramentas como curl, Postman ou escreva testes unitários.

Além disso, lembre-se de que o uso de database/sql é de baixo nível; para projetos maiores, você pode usar um ORM como GORM ou sqlx para reduzir boilerplate.

Referências

Exercícios

  1. Implemente um middleware de recuperação de pânico (panic recovery) que capture pânicos nos handlers e retorne um erro 500 com mensagem amigável.
  2. ✓ Resposta:
    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, "Internal Server Error", http.StatusInternalServerError)
                }
            }()
            next.ServeHTTP(w, r)
        })
    }
    // Uso: handler := recoveryMiddleware(mux)
  3. Adicione a funcionalidade de buscar produtos por nome (ex: GET /products?name=apple).
  4. ✓ Resposta:
    func listProducts(w http.ResponseWriter, r *http.Request) {
        name := r.URL.Query().Get("name")
        var rows *sql.Rows
        var err error
        if name != "" {
            rows, err = db.Query("SELECT id, name, price FROM products WHERE name LIKE ?", "%"+name+"%")
        } else {
            rows, err = db.Query("SELECT id, name, price FROM products")
        }
        // ... resto igual
  5. Modifique o middleware de autenticação para aceitar um token via query string também (ex: ?token=secret-token).
  6. ✓ Resposta:
    func authMiddleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := r.Header.Get("Authorization")
            if token == "" {
                token = r.URL.Query().Get("token")
            }
            if token != "secret-token" {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
  7. Crie um endpoint GET /health que retorne {"status": "ok"} e que não exija autenticação.
  8. ✓ Resposta:
    func healthHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
    }
    
    // No main:
    mux.HandleFunc("GET /health", healthHandler)
    // Aplique os middlewares apenas no mux principal, mas exclua o health:
    // handler := loggingMiddleware(authMiddleware(mux))
    // Mas para não proteger o health, você pode criar um sub-mux ou aplicar o auth apenas em rotas específicas.
    // Uma solução simples: registre o health antes de aplicar o auth, ou crie um mux separado.
  9. Escreva um teste para o handler getProduct usando o pacote net/http/httptest.
  10. ✓ Resposta:
    func TestGetProduct(t *testing.T) {
        // Configurar banco de teste (em memória)
        db, _ = sql.Open("sqlite3", ":memory:")
        createTable()
        // Inserir produto de teste
        db.Exec("INSERT INTO products (name, price) VALUES ('Test', 10.0)")
    
        req := httptest.NewRequest("GET", "/products/1", nil)
        rr := httptest.NewRecorder()
        getProduct(rr, req)
    
        if rr.Code != http.StatusOK {
            t.Errorf("expected 200, got %d", rr.Code)
        }
    
        var p Product
        json.Unmarshal(rr.Body.Bytes(), &p)
        if p.Name != "Test" {
            t.Errorf("expected name Test, got %s", p.Name)
        }
    }