Pular para o conteúdo
Marcações:

API Real: Go e PostgreSQL (CRUD Prático)

Este guia monta uma API REST em Go com PostgreSQL, sem ORM e sem framework, cobrindo as quatro operações de CRUD ligadas a rotas HTTP de verdade. A versão anterior usava o gorilla/mux. Desde o Go 1.22, a biblioteca padrão resolve roteamento por método e parâmetros de caminho, então dá para dispensar a dependência.

O que você precisa

  • Go 1.22 ou mais novo;
  • um PostgreSQL acessível (instruções em postgresql.org/download);
  • o driver github.com/lib/pq, um driver PostgreSQL para database/sql.
go mod init meunome/loja
go get github.com/lib/pq

Crie a tabela:

CREATE TABLE products (
    id    SERIAL PRIMARY KEY,
    name  TEXT NOT NULL,
    price NUMERIC(10,2) NOT NULL
);

Roteamento na biblioteca padrão

Segundo o blog do Go sobre as melhorias de roteamento, a partir da versão 1.22 o ServeMux aceita o método no próprio padrão, como "GET /posts/{id}", e curingas com {id}. O valor sai de req.PathValue("id"). Quando dois padrões se sobrepõem, vale o mais específico.

O código completo

package main

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

    _ "github.com/lib/pq"
)

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

type server struct{ db *sql.DB }

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

func (s *server) create(w http.ResponseWriter, r *http.Request) {
    var p Product
    if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
        http.Error(w, "JSON inválido", http.StatusBadRequest)
        return
    }
    err := s.db.QueryRow(
        `INSERT INTO products (name, price) VALUES ($1, $2) RETURNING id`,
        p.Name, p.Price,
    ).Scan(&p.ID)
    if err != nil {
        http.Error(w, "erro ao salvar", http.StatusInternalServerError)
        return
    }
    writeJSON(w, http.StatusCreated, p)
}

func (s *server) get(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        http.Error(w, "id inválido", http.StatusBadRequest)
        return
    }
    var p Product
    err = s.db.QueryRow(`SELECT id, name, price FROM products WHERE id = $1`, id).
        Scan(&p.ID, &p.Name, &p.Price)
    if errors.Is(err, sql.ErrNoRows) {
        http.Error(w, "produto não encontrado", http.StatusNotFound)
        return
    }
    if err != nil {
        http.Error(w, "erro ao consultar", http.StatusInternalServerError)
        return
    }
    writeJSON(w, http.StatusOK, p)
}

func (s *server) update(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        http.Error(w, "id inválido", http.StatusBadRequest)
        return
    }
    var p Product
    if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
        http.Error(w, "JSON inválido", http.StatusBadRequest)
        return
    }
    res, err := s.db.Exec(`UPDATE products SET name = $2, price = $3 WHERE id = $1`, id, p.Name, p.Price)
    if err != nil {
        http.Error(w, "erro ao atualizar", http.StatusInternalServerError)
        return
    }
    if n, _ := res.RowsAffected(); n == 0 {
        http.Error(w, "produto não encontrado", http.StatusNotFound)
        return
    }
    p.ID = id
    writeJSON(w, http.StatusOK, p)
}

func (s *server) remove(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        http.Error(w, "id inválido", http.StatusBadRequest)
        return
    }
    res, err := s.db.Exec(`DELETE FROM products WHERE id = $1`, id)
    if err != nil {
        http.Error(w, "erro ao remover", http.StatusInternalServerError)
        return
    }
    if n, _ := res.RowsAffected(); n == 0 {
        http.Error(w, "produto não encontrado", http.StatusNotFound)
        return
    }
    w.WriteHeader(http.StatusNoContent)
}

func main() {
    // exemplo: postgres://usuario:senha@localhost:5432/loja?sslmode=disable
    db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    if err := db.Ping(); err != nil {
        log.Fatal(err)
    }

    s := &server{db: db}
    mux := http.NewServeMux()
    mux.HandleFunc("POST /products", s.create)
    mux.HandleFunc("GET /products/{id}", s.get)
    mux.HandleFunc("PUT /products/{id}", s.update)
    mux.HandleFunc("DELETE /products/{id}", s.remove)

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

Pontos que importam

  • Conexão: o README do pq aceita tanto o formato chave=valor quanto a URL postgres://. A URL vai numa variável de ambiente, para a senha não ficar no código;
  • Ping: sql.Open só prepara o pool. É o db.Ping() que confirma que o banco responde;
  • SQL injection: todas as consultas usam $1, $2 como parâmetros, nunca concatenação de string, o que evita a injeção por construção;
  • Códigos HTTP: 201 na criação, 404 quando o registro não existe, 204 na remoção.

Testando

export DATABASE_URL="postgres://usuario:senha@localhost:5432/loja?sslmode=disable"
go run main.go

curl -X POST localhost:8080/products -d '{"name":"Caneta","price":3.5}'
curl localhost:8080/products/1

O que falta antes de produção

Autenticação e validação de entrada: do jeito que está, qualquer um que souber a URL altera seus produtos. Faltam também listagem com paginação, tempos limite nas consultas e migrações de esquema versionadas.

Perguntas comuns

Por que não um ORM? O database/sql dá controle total sobre a consulta e basta para APIs pequenas. Em projetos grandes, um ORM pode reduzir código repetitivo.

E o gorilla/mux? O README o descreve como um roteador e comparador de URLs, com mux.Vars() e o matcher Methods(). Continua funcionando, e ainda faz sentido se você precisa de recursos além do que o ServeMux oferece. Para o caso básico, a biblioteca padrão basta.

E o driver? O lib/pq é um dos drivers para database/sql. Há outras opções, como o pgx, e vale comparar o estado de manutenção antes de escolher para um projeto novo.

Se prefere um framework, veja o tutorial do Gin.