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 paradatabase/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
pqaceita tanto o formato chave=valor quanto a URLpostgres://. A URL vai numa variável de ambiente, para a senha não ficar no código; - Ping:
sql.Opensó prepara o pool. É odb.Ping()que confirma que o banco responde; - SQL injection: todas as consultas usam
$1,$2como 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.