Capítulo 09: Clean Architecture em Go: Handlers, Services e Repositories
Especialização em Backend com Go & Gin • Gin Framework & Go 1.22+ • GORM, Clean Architecture, Concorrência e Alta Performance
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente HTTP / Frontend"] --> B["Framework Gin (HTTP Delivery)"]
subgraph DELIVERY["Camada 1: Handlers / Controllers"]
B --> C["UsuarioHandler.Criar(c *gin.Context)"]
C --> D["Validação de DTO de Entrada (ShouldBindJSON)"]
end
subgraph USE_CASES["Camada 2: Use Cases / Services"]
D --> E["UsuarioService.CadastrarUsuario(ctx, dto)"]
E --> F["Regras de Negócio: Hashing de Senha, Validação de Domínio"]
end
subgraph DOMAIN["Camada 3: Domínio Puro (Entities)"]
G["models.Usuario (Entidade de Negócio)"]
end
subgraph REPOSITORIES["Camada 4: Repositories (Persistência)"]
E --> H["Interface: UsuarioRepository (Ports)"]
H --> I["Implementação Concreta: GormUsuarioRepository (Adapters)"]
I --> J[("Banco de Dados PostgreSQL")]
end
E -.-> G
I -.-> G
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style E fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style F fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style G fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style H fill:#fffde7,stroke:#fbc02d,stroke-width:2px
style I fill:#e0f2f1,stroke:#009688,stroke-width:2px
style J fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
🏛️ 1. Princípios de Clean Architecture Idiomática em Go
Em sistemas de grande porte, acoplar a lógica de negócio diretamente aos handlers do Gin ou aos métodos do GORM cria uma base de código rígida, difícil de testar e dependente de bibliotecas externas. A Clean Architecture (Arquitetura Limpa / Portas e Adaptadores) preconiza que a regra de negócio central da empresa deve ser totalmente agnóstica a bancos de dados, frameworks web e bibliotecas de terceiros.
Em Go, a Clean Architecture estrutura-se em quatro camadas com dependências unidirecionais voltadas para dentro:
| Camada | Responsabilidade Central | O que PODE Conhecer | O que NÃO PODE Conhecer |
|---|---|---|---|
| Domain (Entities) | Modelos de dados e regras de negócio essenciais | Apenas a biblioteca padrão Go (time, errors) |
Nem Gin, nem GORM, nem SQL. |
| Repositories | Interface de persistência (Port) e implementação GORM (Adapter) | Entidades do Domínio e Drivers de Banco | Não conhece Gin nem HTTP. |
| Services (Use Cases) | Orquestração de casos de uso e regras de aplicação | Repositórios via interfaces e Entidades | Não conhece *gin.Context nem SQL. |
| Delivery (Handlers) | Parse de requisições HTTP, headers, status codes e JSON | DTOs e Interfaces dos Services | Não acessa o banco de dados diretamente. |
⚙️ 2. O Poder das Interfaces Implícitas de Go
Uma das características mais elegantes da linguagem Go é que interfaces são satisfeitas implicitamente: uma struct não precisa declarar implements UsuarioRepository. Basta que ela possua os métodos com a mesma assinatura para ser aceita como implementação válida. Isso viabiliza o princípio da Inversão de Dependência (IoC) sem acoplamento.
1. Interface do Repositório (Camada de Domínio/Portas)
package domain
import (
"context"
"time"
)
type Usuario struct {
ID uint
Nome string
Email string
SenhaHash string
CriadoEm time.Time
}
type UsuarioRepository interface {
Salvar(ctx context.Context, u *Usuario) error
BuscarPorEmail(ctx context.Context, email string) (*Usuario, error)
}
2. Camada de Serviço (Regras de Aplicação)
package service
import (
"context"
"errors"
"golang.org/x/crypto/bcrypt"
"meu-modulo/domain"
)
type UsuarioService interface {
Registrar(ctx context.Context, nome, email, senhaPlana string) (*domain.Usuario, error)
}
type usuarioService struct {
repo domain.UsuarioRepository // Injeção de dependência via interface
}
func NovoUsuarioService(repo domain.UsuarioRepository) UsuarioService {
return &usuarioService{repo: repo}
}
func (s *usuarioService) Registrar(ctx context.Context, nome, email, senhaPlana string) (*domain.Usuario, error) {
// 1. Validação de Regra de Negócio
existente, _ := s.repo.BuscarPorEmail(ctx, email)
if existente != nil {
return nil, errors.New("o e-mail informado já está cadastrado no sistema")
}
// 2. Criptografia da Senha
hash, err := bcrypt.GenerateFromPassword([]byte(senhaPlana), bcrypt.DefaultCost)
if err != nil {
return nil, err
}
// 3. Criação da Entidade
novo := &domain.Usuario{
Nome: nome,
Email: email,
SenhaHash: string(hash),
}
if err := s.repo.Salvar(ctx, novo); err != nil {
return nil, err
}
return novo, nil
}
🚀 3. Camada de Entrega: Handler Gin Desacoplado
O handler HTTP limita-se a receber o payload, validar o formato com Pydantic/Validator, invocar o serviço e serializar a resposta:
package handler
import (
"net/http"
"github.com/gin-gonic/gin"
"meu-modulo/service"
)
type CriarUsuarioRequest struct {
Nome string `json:"nome" binding:"required,min=3"`
Email string `json:"email" binding:"required,email"`
Senha string `json:"senha" binding:"required,min=8"`
}
type UsuarioHandler struct {
servico service.UsuarioService
}
func NovoUsuarioHandler(servico service.UsuarioService) *UsuarioHandler {
return &UsuarioHandler{servico: servico}
}
func (h *UsuarioHandler) Registrar(c *gin.Context) {
var req CriarUsuarioRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"erro": "payload inválido", "detalhes": err.Error()})
return
}
usuario, err := h.servico.Registrar(c.Request.Context(), req.Nome, req.Email, req.Senha)
if err != nil {
c.JSON(http.StatusUnprocessableEntity, gin.H{"erro": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{
"id": usuario.ID,
"nome": usuario.Nome,
"email": usuario.Email,
})
}
🧪 4. Testabilidade Absoluta via Mocks de Interfaces
Com essa arquitetura, a camada de serviço pode ser testada em 100% dos seus caminhos de código em microssegundos, sem depender de banco de dados real nem de contêineres Docker:
// Exemplo de Mock de Repositório para Testes Unitários
type mockUsuarioRepo struct {
usuarios map[string]*domain.Usuario
}
func (m *mockUsuarioRepo) Salvar(ctx context.Context, u *domain.Usuario) error {
u.ID = 1
m.usuarios[u.Email] = u
return nil
}
func (m *mockUsuarioRepo) BuscarPorEmail(ctx context.Context, email string) (*domain.Usuario, error) {
if u, ok := m.usuarios[email]; ok {
return u, nil
}
return nil, nil
}