Go: Projeto Abrangente: API de comércio eletrônico (Parte 1)
Última atualização: 2026-08-26
É isso aí em termos de teoria. Agora vamos começar: vamos criar uma API de comércio eletrônico de verdade do zero usando Go.
Bob decidiu criar sua própria plataforma de comércio eletrônico. Três semanas, três histórias e um backend totalmente em Go.
1. Você aprenderá
- Análise de requisitos e projeto de arquitetura da API de comércio eletrônico
- Implementação completa da arquitetura de quatro camadas: Modelo → Repositório → Serviço → Manipulador
- Endpoints CRUD + validação de parâmetros
- Tipos de erro personalizados + tratamento unificado de erros
- Testes unitários (repositório de simulações)
2. História: O primeiro dia de Bob como empreendedor
(1) Desafio: “Vou criar um MVP primeiro e lançá-lo em duas semanas.”
Bob pediu demissão do emprego para abrir seu próprio negócio — uma plataforma de comércio eletrônico:
“No primeiro dia, criei um monólito — com toda a lógica no
main.go. No sétimo dia, o código já tinha chegado a 3.000 linhas, e eu não conseguia distinguir quais partes eram da lógica de pedidos e quais eram da lógica do usuário. Eu queria adicionar um recurso de ‘cupom’, mas, depois de modificar seis arquivos, ainda não tinha terminado. Meu sócio disse: ‘Lançamos em duas semanas’, mas acho que até dois meses talvez seja pouco.”
Mas, depois de se acalmar e refletir sobre a arquitetura de quatro camadas do Go, ele decidiu refatorar o código:
// Refactoring plan:
// cmd/server/ → entry point
// internal/handler/ → HTTP handlers
// internal/service/ → business logic
// internal/repository/ → data access
// pkg/model/ → domain models
(2) Objetivos da aula: Operações CRUD básicas para usuários, produtos e pedidos
Meta da Semana 1: Criar uma estrutura completa e implementar as operações CRUD para os três recursos principais.
POST /api/v1/register → User registration
POST /api/v1/login → User login
GET /api/v1/products → Product list
GET /api/v1/products/{id} → Product details
POST /api/v1/orders → Create order
GET /api/v1/orders/{id} → Order details
GET /api/v1/orders → My orders list
3. Estrutura do projeto
ecommerce/
├── cmd/
│ └── server/
│ └── main.go # Entry + dependency injection
├── internal/
│ ├── handler/ # HTTP layer
│ │ ├── user.go
│ │ ├── product.go
│ │ ├── order.go
│ │ └── response.go # Unified response utilities
│ ├── service/ # Business layer
│ │ ├── user.go
│ │ ├── product.go
│ │ ├── order.go
│ │ └── errors.go # Business error definitions
│ └── repository/ # Data layer
│ ├── user.go
│ ├── product.go
│ └── order.go
├── pkg/
│ └── model/ # Domain models
│ ├── user.go
│ ├── product.go
│ └── order.go
├── go.mod
└── go.sum
4. Implementação completa
(1) ▶ Exemplo: Camada do modelo
// pkg/model/user.go
package model
type User struct {
ID int `json:"id"`
Email string `json:"email"`
Password string `json:"-"` // Not returned to client
Name string `json:"name"`
}
type RegisterRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Name string `json:"name"`
}
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
// pkg/model/product.go
package model
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Price float64 `json:"price"`
Stock int `json:"stock"`
}
// pkg/model/order.go
package model
type Order struct {
ID int `json:"id"`
UserID int `json:"user_id"`
ProductID int `json:"product_id"`
Quantity int `json:"quantity"`
TotalPrice float64 `json:"total_price"`
Status string `json:"status"`
}
type CreateOrderRequest struct {
ProductID int `json:"product_id"`
Quantity int `json:"quantity"`
}
(2) ▶ Exemplo: Camada de repositório
// internal/repository/user.go
package repository
import (
"database/sql"
"ecommerce/pkg/model"
)
type UserRepository interface {
FindByID(id int) (*model.User, error)
FindByEmail(email string) (*model.User, error)
Create(user *model.User) error
}
type userRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) UserRepository {
return &userRepository{db: db}
}
func (r *userRepository) FindByID(id int) (*model.User, error) {
u := &model.User{}
err := r.db.QueryRow("SELECT id, email, password, name FROM users WHERE id = ?", id).
Scan(&u.ID, &u.Email, &u.Password, &u.Name)
if err == sql.ErrNoRows {
return nil, nil
}
return u, err
}
func (r *userRepository) FindByEmail(email string) (*model.User, error) {
u := &model.User{}
err := r.db.QueryRow("SELECT id, email, password, name FROM users WHERE email = ?", email).
Scan(&u.ID, &u.Email, &u.Password, &u.Name)
if err == sql.ErrNoRows {
return nil, nil
}
return u, err
}
func (r *userRepository) Create(user *model.User) error {
result, err := r.db.Exec(
"INSERT INTO users (email, password, name) VALUES (?, ?, ?)",
user.Email, user.Password, user.Name,
)
if err != nil {
return err
}
id, _ := result.LastInsertId()
user.ID = int(id)
return nil
}
// internal/repository/product.go
package repository
import (
"database/sql"
"ecommerce/pkg/model"
)
type ProductRepository interface {
FindByID(id int) (*model.Product, error)
List() ([]*model.Product, error)
Create(product *model.Product) error
UpdateStock(id int, stock int) error
}
type productRepository struct {
db *sql.DB
}
func NewProductRepository(db *sql.DB) ProductRepository {
return &productRepository{db: db}
}
func (r *productRepository) FindByID(id int) (*model.Product, error) {
p := &model.Product{}
err := r.db.QueryRow("SELECT id, name, description, price, stock FROM products WHERE id = ?", id).
Scan(&p.ID, &p.Name, &p.Description, &p.Price, &p.Stock)
if err == sql.ErrNoRows {
return nil, nil
}
return p, err
}
func (r *productRepository) List() ([]*model.Product, error) {
rows, err := r.db.Query("SELECT id, name, description, price, stock FROM products")
if err != nil {
return nil, err
}
defer rows.Close()
var products []*model.Product
for rows.Next() {
p := &model.Product{}
if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.Price, &p.Stock); err != nil {
return nil, err
}
products = append(products, p)
}
return products, rows.Err()
}
func (r *productRepository) Create(product *model.Product) error {
result, err := r.db.Exec(
"INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)",
product.Name, product.Description, product.Price, product.Stock,
)
if err != nil {
return err
}
id, _ := result.LastInsertId()
product.ID = int(id)
return nil
}
func (r *productRepository) UpdateStock(id int, stock int) error {
_, err := r.db.Exec("UPDATE products SET stock = ? WHERE id = ?", stock, id)
return err
}
// internal/repository/order.go
package repository
import (
"database/sql"
"ecommerce/pkg/model"
)
type OrderRepository interface {
FindByID(id int) (*model.Order, error)
ListByUserID(userID int) ([]*model.Order, error)
Create(order *model.Order) error
}
type orderRepository struct {
db *sql.DB
}
func NewOrderRepository(db *sql.DB) OrderRepository {
return &orderRepository{db: db}
}
func (r *orderRepository) FindByID(id int) (*model.Order, error) {
o := &model.Order{}
err := r.db.QueryRow(
"SELECT id, user_id, product_id, quantity, total_price, status FROM orders WHERE id = ?", id,
).Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.TotalPrice, &o.Status)
if err == sql.ErrNoRows {
return nil, nil
}
return o, err
}
func (r *orderRepository) ListByUserID(userID int) ([]*model.Order, error) {
rows, err := r.db.Query(
"SELECT id, user_id, product_id, quantity, total_price, status FROM orders WHERE user_id = ?",
userID,
)
if err != nil {
return nil, err
}
defer rows.Close()
var orders []*model.Order
for rows.Next() {
o := &model.Order{}
if err := rows.Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.TotalPrice, &o.Status); err != nil {
return nil, err
}
orders = append(orders, o)
}
return orders, rows.Err()
}
func (r *orderRepository) Create(order *model.Order) error {
result, err := r.db.Exec(
"INSERT INTO orders (user_id, product_id, quantity, total_price, status) VALUES (?, ?, ?, ?, ?)",
order.UserID, order.ProductID, order.Quantity, order.TotalPrice, order.Status,
)
if err != nil {
return err
}
id, _ := result.LastInsertId()
order.ID = int(id)
return nil
}
(3) Camada de Serviço
// internal/service/errors.go
package service
import "errors"
var (
ErrEmailTaken = errors.New("email already registered")
ErrInvalidCreds = errors.New("invalid email or password")
ErrProductNotFound = errors.New("product not found")
ErrInsufficientStock = errors.New("insufficient stock")
ErrOrderNotFound = errors.New("order not found")
ErrUnauthorized = errors.New("unauthorized")
)
// internal/service/user.go
package service
import (
"ecommerce/internal/repository"
"ecommerce/pkg/model"
"strings"
)
type UserService struct {
userRepo repository.UserRepository
}
func NewUserService(userRepo repository.UserRepository) *UserService {
return &UserService{userRepo: userRepo}
}
func (s *UserService) Register(req model.RegisterRequest) (*model.User, error) {
if strings.TrimSpace(req.Email) == "" {
return nil, errors.New("email is required")
}
if strings.TrimSpace(req.Password) == "" {
return nil, errors.New("password is required")
}
if strings.TrimSpace(req.Name) == "" {
return nil, errors.New("name is required")
}
existing, _ := s.userRepo.FindByEmail(req.Email)
if existing != nil {
return nil, ErrEmailTaken
}
user := &model.User{
Email: req.Email,
Password: req.Password, // Use bcrypt in production
Name: req.Name,
}
if err := s.userRepo.Create(user); err != nil {
return nil, err
}
return user, nil
}
func (s *UserService) Login(req model.LoginRequest) (*model.User, error) {
user, err := s.userRepo.FindByEmail(req.Email)
if err != nil {
return nil, err
}
if user == nil || user.Password != req.Password {
return nil, ErrInvalidCreds
}
return user, nil
}
func (s *UserService) GetUserByID(id int) (*model.User, error) {
return s.userRepo.FindByID(id)
}
// internal/service/product.go
package service
import (
"ecommerce/internal/repository"
"ecommerce/pkg/model"
)
type ProductService struct {
productRepo repository.ProductRepository
}
func NewProductService(productRepo repository.ProductRepository) *ProductService {
return &ProductService{productRepo: productRepo}
}
func (s *ProductService) ListProducts() ([]*model.Product, error) {
return s.productRepo.List()
}
func (s *ProductService) GetProduct(id int) (*model.Product, error) {
product, err := s.productRepo.FindByID(id)
if err != nil {
return nil, err
}
if product == nil {
return nil, ErrProductNotFound
}
return product, nil
}
// internal/service/order.go
package service
import (
"ecommerce/internal/repository"
"ecommerce/pkg/model"
)
type OrderService struct {
orderRepo repository.OrderRepository
productRepo repository.ProductRepository
userRepo repository.UserRepository
}
func NewOrderService(
orderRepo repository.OrderRepository,
productRepo repository.ProductRepository,
userRepo repository.UserRepository,
) *OrderService {
return &OrderService{
orderRepo: orderRepo,
productRepo: productRepo,
userRepo: userRepo,
}
}
func (s *OrderService) CreateOrder(userID int, req model.CreateOrderRequest) (*model.Order, error) {
if req.Quantity <= 0 {
return nil, errors.New("quantity must be positive")
}
product, err := s.productRepo.FindByID(req.ProductID)
if err != nil {
return nil, err
}
if product == nil {
return nil, ErrProductNotFound
}
if product.Stock < req.Quantity {
return nil, ErrInsufficientStock
}
totalPrice := product.Price * float64(req.Quantity)
order := &model.Order{
UserID: userID,
ProductID: req.ProductID,
Quantity: req.Quantity,
TotalPrice: totalPrice,
Status: "pending",
}
if err := s.orderRepo.Create(order); err != nil {
return nil, err
}
// Deduct stock
newStock := product.Stock - req.Quantity
if err := s.productRepo.UpdateStock(product.ID, newStock); err != nil {
return nil, err
}
return order, nil
}
func (s *OrderService) GetOrder(orderID, userID int) (*model.Order, error) {
order, err := s.orderRepo.FindByID(orderID)
if err != nil {
return nil, err
}
if order == nil {
return nil, ErrOrderNotFound
}
if order.UserID != userID {
return nil, ErrUnauthorized
}
return order, nil
}
func (s *OrderService) ListUserOrders(userID int) ([]*model.Order, error) {
return s.orderRepo.ListByUserID(userID)
}
(4) Camada de manipuladores
// internal/handler/response.go
package handler
import (
"encoding/json"
"net/http"
)
type APIResponse struct {
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(APIResponse{Data: data})
}
func writeError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(APIResponse{Error: msg})
}
// internal/handler/user.go
package handler
import (
"encoding/json"
"errors"
"net/http"
"ecommerce/internal/service"
"ecommerce/pkg/model"
)
type UserHandler struct {
userSvc *service.UserService
}
func NewUserHandler(userSvc *service.UserService) *UserHandler {
return &UserHandler{userSvc: userSvc}
}
func (h *UserHandler) Register(mux *http.ServeMux) {
mux.HandleFunc("POST /api/v1/register", h.RegisterUser)
mux.HandleFunc("POST /api/v1/login", h.LoginUser)
}
func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
var req model.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
user, err := h.userSvc.Register(req)
if errors.Is(err, service.ErrEmailTaken) {
writeError(w, http.StatusConflict, err.Error())
return
}
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, user)
}
func (h *UserHandler) LoginUser(w http.ResponseWriter, r *http.Request) {
var req model.LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
user, err := h.userSvc.Login(req)
if errors.Is(err, service.ErrInvalidCreds) {
writeError(w, http.StatusUnauthorized, err.Error())
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "login failed")
return
}
writeJSON(w, http.StatusOK, user)
}
// internal/handler/product.go
package handler
import (
"errors"
"net/http"
"strconv"
"ecommerce/internal/service"
)
type ProductHandler struct {
productSvc *service.ProductService
}
func NewProductHandler(productSvc *service.ProductService) *ProductHandler {
return &ProductHandler{productSvc: productSvc}
}
func (h *ProductHandler) Register(mux *http.ServeMux) {
mux.HandleFunc("GET /api/v1/products", h.ListProducts)
mux.HandleFunc("GET /api/v1/products/{id}", h.GetProduct)
}
func (h *ProductHandler) ListProducts(w http.ResponseWriter, r *http.Request) {
products, err := h.productSvc.ListProducts()
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list products")
return
}
writeJSON(w, http.StatusOK, products)
}
func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid product ID")
return
}
product, err := h.productSvc.GetProduct(id)
if errors.Is(err, service.ErrProductNotFound) {
writeError(w, http.StatusNotFound, err.Error())
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to get product")
return
}
writeJSON(w, http.StatusOK, product)
}
// internal/handler/order.go
package handler
import (
"encoding/json"
"errors"
"net/http"
"strconv"
"ecommerce/internal/service"
"ecommerce/pkg/model"
)
type OrderHandler struct {
orderSvc *service.OrderService
}
func NewOrderHandler(orderSvc *service.OrderService) *OrderHandler {
return &OrderHandler{orderSvc: orderSvc}
}
func (h *OrderHandler) Register(mux *http.ServeMux) {
mux.HandleFunc("POST /api/v1/orders", h.CreateOrder)
mux.HandleFunc("GET /api/v1/orders", h.ListOrders)
mux.HandleFunc("GET /api/v1/orders/{id}", h.GetOrder)
}
// Simulate getting userID from Context (JWT implemented in Part 2)
func getUserID(r *http.Request) int {
if id, ok := r.Context().Value("user_id").(int); ok {
return id
}
return 1 // Default user (temporary for Part 1)
}
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
var req model.CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON")
return
}
order, err := h.orderSvc.CreateOrder(userID, req)
if errors.Is(err, service.ErrProductNotFound) {
writeError(w, http.StatusNotFound, err.Error())
return
}
if errors.Is(err, service.ErrInsufficientStock) {
writeError(w, http.StatusConflict, err.Error())
return
}
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusCreated, order)
}
func (h *OrderHandler) GetOrder(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "invalid order ID")
return
}
order, err := h.orderSvc.GetOrder(id, userID)
if errors.Is(err, service.ErrOrderNotFound) {
writeError(w, http.StatusNotFound, err.Error())
return
}
if errors.Is(err, service.ErrUnauthorized) {
writeError(w, http.StatusForbidden, err.Error())
return
}
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to get order")
return
}
writeJSON(w, http.StatusOK, order)
}
func (h *OrderHandler) ListOrders(w http.ResponseWriter, r *http.Request) {
userID := getUserID(r)
orders, err := h.orderSvc.ListUserOrders(userID)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to list orders")
return
}
writeJSON(w, http.StatusOK, orders)
}
(5) Principal (Injeção de Dependências)
⚙️ Pré-requisito: Execute
go get github.com/mattn/go-sqlite3(requer CGO; como alternativa, usemodernc.org/sqlitepara um driver exclusivamente de Go)
package main
import (
"database/sql"
"log"
"net/http"
"ecommerce/internal/handler"
"ecommerce/internal/repository"
"ecommerce/internal/service"
_ "github.com/mattn/go-sqlite3"
)
func main() {
db, err := sql.Open("sqlite3", "./ecommerce.db")
if err != nil {
log.Fatal(err)
}
defer db.Close()
initDB(db)
// Dependency injection
userRepo := repository.NewUserRepository(db)
productRepo := repository.NewProductRepository(db)
orderRepo := repository.NewOrderRepository(db)
userSvc := service.NewUserService(userRepo)
productSvc := service.NewProductService(productRepo)
orderSvc := service.NewOrderService(orderRepo, productRepo, userRepo)
userHandler := handler.NewUserHandler(userSvc)
productHandler := handler.NewProductHandler(productSvc)
orderHandler := handler.NewOrderHandler(orderSvc)
mux := http.NewServeMux()
userHandler.Register(mux)
productHandler.Register(mux)
orderHandler.Register(mux)
log.Println("E-commerce API listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func initDB(db *sql.DB) {
schema := `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
name TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
price REAL NOT NULL,
stock INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER NOT NULL,
total_price REAL NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES products(id)
);`
if _, err := db.Exec(schema); err != nil {
log.Fatal("initDB:", err)
}
}
(6) Testes unitários (repositório de simulação)
// internal/service/order_test.go
package service
import (
"testing"
"ecommerce/pkg/model"
)
// mock Repository
type mockProductRepo struct {
products map[int]*model.Product
}
func (m *mockProductRepo) FindByID(id int) (*model.Product, error) {
return m.products[id], nil
}
func (m *mockProductRepo) List() ([]*model.Product, error) {
var result []*model.Product
for _, p := range m.products {
result = append(result, p)
}
return result, nil
}
func (m *mockProductRepo) Create(product *model.Product) error {
product.ID = len(m.products) + 1
m.products[product.ID] = product
return nil
}
func (m *mockProductRepo) UpdateStock(id int, stock int) error {
if p, ok := m.products[id]; ok {
p.Stock = stock
}
return nil
}
type mockOrderRepo struct {
orders []*model.Order
}
func (m *mockOrderRepo) FindByID(id int) (*model.Order, error) {
for _, o := range m.orders {
if o.ID == id {
return o, nil
}
}
return nil, nil
}
func (m *mockOrderRepo) ListByUserID(userID int) ([]*model.Order, error) {
var result []*model.Order
for _, o := range m.orders {
if o.UserID == userID {
result = append(result, o)
}
}
return result, nil
}
func (m *mockOrderRepo) Create(order *model.Order) error {
order.ID = len(m.orders) + 1
m.orders = append(m.orders, order)
return nil
}
func TestCreateOrder_Success(t *testing.T) {
productRepo := &mockProductRepo{
products: map[int]*model.Product{
1: {ID: 1, Name: "Laptop", Price: 999.99, Stock: 10},
},
}
orderRepo := &mockOrderRepo{}
svc := NewOrderService(orderRepo, productRepo, nil)
order, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 1, Quantity: 2})
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if order.TotalPrice != 1999.98 {
t.Errorf("expected total 1999.98, got %.2f", order.TotalPrice)
}
}
func TestCreateOrder_InsufficientStock(t *testing.T) {
productRepo := &mockProductRepo{
products: map[int]*model.Product{
1: {ID: 1, Name: "Laptop", Price: 999.99, Stock: 1},
},
}
orderRepo := &mockOrderRepo{}
svc := NewOrderService(orderRepo, productRepo, nil)
_, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 1, Quantity: 5})
if err != ErrInsufficientStock {
t.Errorf("expected ErrInsufficientStock, got %v", err)
}
}
func TestCreateOrder_ProductNotFound(t *testing.T) {
productRepo := &mockProductRepo{products: make(map[int]*model.Product)}
orderRepo := &mockOrderRepo{}
svc := NewOrderService(orderRepo, productRepo, nil)
_, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 999, Quantity: 1})
if err != ErrProductNotFound {
t.Errorf("expected ErrProductNotFound, got %v", err)
}
}
type UserRepository interface{...}), você pode injetar implementações simuladas em seus testes. Essa é uma grande vantagem das interfaces implícitas do Go — você não precisa de uma estrutura de simulação; basta escrever simulações leves manualmente.
❓ Perguntas Frequentes
P: Como o modelo de 4 camadas é implementado em projetos reais? R: Comece pelo Modelo (estrutura de dados) → Repositório (CRUD do banco de dados) → Serviço (regras de negócios) → Manipulador (HTTP). Cada arquivo não deve exceder 200 linhas. Os arquivos são organizados por recurso (usuário/produto/pedido). A função principal é responsável por reunir todas as dependências (injeção de dependências).
P: Como se deve projetar tabelas simuladas para testes de unidade? R: Defina o Repositório usando uma interface e passe uma implementação simulada durante os testes. Armazene os dados simulados na memória usando um mapa. Simule o Repositório ao testar a camada de Serviço e simule o Serviço ao testar a camada de Manipulador. Não é necessária nenhuma estrutura de simulação — as interfaces implícitas do Go tornam a simulação manual muito leve.
P: Quais são os três estilos de tratamento de erros? R: (1) Tipos de erro personalizados (incluindo Código/Mensagem/HTTPStatus) — adequados para projetos grandes; (2) erros sentinela (
var ErrXxx = errors.New(...)) — adequados para projetos de médio porte; (3) erros de string — adequados apenas para projetos pequenos. Este projeto utiliza erros sentinela.
P: Como é a estrutura de recursos RESTful? R: usuários (cadastro/login), produtos (lista/detalhes), pedidos (criar/lista/detalhes). Cada recurso possui seu próprio manipulador, serviço e repositório. As URLs utilizam substantivos no plural:
/api/v1/users,/api/v1/products,/api/v1/orders.
P: Comparação entre bibliotecas de validação? R: (1) Validação manual — neste projeto, usamos
strings.TrimSpacepara a verificação — sem dependências; (2)go-playground/validator— validação baseada em tags, muito poderosa; (3) Estrutura personalizadaValidator. Para projetos pequenos, a validação manual é suficiente.
📖 Resumo
- Estrutura de API para comércio eletrônico: três recursos principais — usuários, produtos e pedidos
- Arquitetura de 4 camadas: Modelo → Repositório → Serviço → Manipulador
- Interface de repositório: facilita a realização de testes com simulações
- Regras de negócios do serviço: Validação → Consulta → Cálculo → Gravação
- Manipulador HTTP: Análise de JSON + Mapeamento de erros + Respostas unificadas
- Testes unitários: simulação do repositório para testar a lógica de negócios do serviço
- Arquivo de entrada
main: Injeção de dependências para montar todas as camadas
📝 Exercícios
-
Básico (Dificuldade ⭐): Execute o código completo desta lição e use
curlpara testar endpoints como cadastro, login, lista de produtos e criação de pedidos. Verifique se todas as operações CRUD funcionam corretamente. -
Avançado (Dificuldade ⭐⭐): Adicione testes de unidade completos à camada de Serviços do sistema. Requisitos: (1) Abranger o cenário de e-mail duplicado para UserService.Register; (2) Abranger o cenário de “e-mail não encontrado” para
ProductService.GetProduct; (3) Abranger o cenário de “não autorizado” paraOrderService.GetOrder; (4) Cobertura de teste > 80%. -
Desafio (Dificuldade ⭐⭐⭐): Adicione um recurso Categoria ao sistema. Requisitos: (1) Adicione um campo CategoryID ao modelo Produto; (2) Implemente operações CRUD para Categorias (funcionalidade de administrador); (3) Implemente o endpoint GET /products?category_id=X para filtrar por categoria; (4) Valide a existência das categorias na camada de serviço; (5) Forneça testes de unidade completos.