Clean Architecture em Go
Clean Architecture é um padrão de design que separa as preocupações do seu código em camadas distintas, tornando-o mais testável, manutenível e independente de frameworks e bancos de dados.
Os Princípios
A Clean Architecture é baseada em alguns princípios fundamentais:
- Independência de frameworks: A arquitetura não depende de bibliotecas externas
- Testabilidade: A lógica de negócio pode ser testada sem UI, banco de dados ou servidor
- Independência de UI: A UI pode mudar sem alterar o resto do sistema
- Independência de banco de dados: Você pode trocar PostgreSQL por MongoDB sem mudar regras de negócio
- Independência de agentes externos: As regras de negócio não sabem nada sobre o mundo externo
As Camadas
┌─────────────────────────────────────────┐
│ Frameworks & Drivers │
│ (HTTP, gRPC, CLI, Banco de Dados) │
├─────────────────────────────────────────┤
│ Interface Adapters │
│ (Controllers, Gateways, Presenters) │
├─────────────────────────────────────────┤
│ Application Business │
│ (Use Cases) │
├─────────────────────────────────────────┤
│ Enterprise Business │
│ (Entities) │
└─────────────────────────────────────────┘
Estrutura do Projeto
project/
├── cmd/
│ └── api/
│ └── main.go
├── internal/
│ ├── domain/ # Entidades e regras de negócio
│ │ ├── user.go
│ │ └── errors.go
│ ├── usecase/ # Casos de uso
│ │ ├── user_usecase.go
│ │ └── interfaces.go
│ ├── repository/ # Implementações de repositório
│ │ ├── postgres/
│ │ │ └── user_repository.go
│ │ └── memory/
│ │ └── user_repository.go
│ └── delivery/ # Handlers HTTP/gRPC
│ └── http/
│ └── user_handler.go
├── pkg/
│ └── database/
│ └── postgres.go
└── go.mod
Camada de Domínio
A camada mais interna contém as entidades e regras de negócio:
// internal/domain/user.go
package domain
import (
"errors"
"regexp"
"time"
)
var (
ErrUserNotFound = errors.New("usuário não encontrado")
ErrInvalidEmail = errors.New("email inválido")
ErrEmailExists = errors.New("email já cadastrado")
)
type User struct {
ID string
Name string
Email string
Password string
CreatedAt time.Time
UpdatedAt time.Time
}
// NewUser cria um novo usuário com validação
func NewUser(name, email, password string) (*User, error) {
if !isValidEmail(email) {
return nil, ErrInvalidEmail
}
return &User{
Name: name,
Email: email,
Password: password,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}, nil
}
// Regra de negócio: validação de email
func isValidEmail(email string) bool {
pattern := `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
matched, _ := regexp.MatchString(pattern, email)
return matched
}
// UpdatePassword atualiza a senha com validação
func (u *User) UpdatePassword(newPassword string) error {
if len(newPassword) < 8 {
return errors.New("senha deve ter pelo menos 8 caracteres")
}
u.Password = newPassword
u.UpdatedAt = time.Now()
return nil
}
Interfaces de Repositório
Definimos interfaces para abstrair o acesso a dados:
// internal/usecase/interfaces.go
package usecase
import (
"context"
"myapp/internal/domain"
)
// UserRepository define as operações de persistência
type UserRepository interface {
Create(ctx context.Context, user *domain.User) error
GetByID(ctx context.Context, id string) (*domain.User, error)
GetByEmail(ctx context.Context, email string) (*domain.User, error)
Update(ctx context.Context, user *domain.User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context, limit, offset int) ([]*domain.User, error)
}
// PasswordHasher define operações de hash de senha
type PasswordHasher interface {
Hash(password string) (string, error)
Compare(hash, password string) bool
}
Casos de Uso
A camada de casos de uso orquestra a lógica de aplicação:
// internal/usecase/user_usecase.go
package usecase
import (
"context"
"github.com/google/uuid"
"myapp/internal/domain"
)
type UserUseCase struct {
userRepo UserRepository
hasher PasswordHasher
}
func NewUserUseCase(repo UserRepository, hasher PasswordHasher) *UserUseCase {
return &UserUseCase{
userRepo: repo,
hasher: hasher,
}
}
type CreateUserInput struct {
Name string
Email string
Password string
}
func (uc *UserUseCase) Create(ctx context.Context, input CreateUserInput) (*domain.User, error) {
// Verificar se email já existe
existing, _ := uc.userRepo.GetByEmail(ctx, input.Email)
if existing != nil {
return nil, domain.ErrEmailExists
}
// Criar entidade com validação de domínio
user, err := domain.NewUser(input.Name, input.Email, input.Password)
if err != nil {
return nil, err
}
// Gerar ID
user.ID = uuid.New().String()
// Hash da senha
hashedPassword, err := uc.hasher.Hash(input.Password)
if err != nil {
return nil, err
}
user.Password = hashedPassword
// Persistir
if err := uc.userRepo.Create(ctx, user); err != nil {
return nil, err
}
return user, nil
}
func (uc *UserUseCase) GetByID(ctx context.Context, id string) (*domain.User, error) {
user, err := uc.userRepo.GetByID(ctx, id)
if err != nil {
return nil, err
}
if user == nil {
return nil, domain.ErrUserNotFound
}
return user, nil
}
Implementação de Repositório
// internal/repository/postgres/user_repository.go
package postgres
import (
"context"
"database/sql"
"myapp/internal/domain"
)
type UserRepository struct {
db *sql.DB
}
func NewUserRepository(db *sql.DB) *UserRepository {
return &UserRepository{db: db}
}
func (r *UserRepository) Create(ctx context.Context, user *domain.User) error {
query := `
INSERT INTO users (id, name, email, password, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6)
`
_, err := r.db.ExecContext(ctx, query,
user.ID, user.Name, user.Email, user.Password,
user.CreatedAt, user.UpdatedAt,
)
return err
}
func (r *UserRepository) GetByID(ctx context.Context, id string) (*domain.User, error) {
query := `SELECT id, name, email, password, created_at, updated_at FROM users WHERE id = $1`
user := &domain.User{}
err := r.db.QueryRowContext(ctx, query, id).Scan(
&user.ID, &user.Name, &user.Email, &user.Password,
&user.CreatedAt, &user.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return user, nil
}
func (r *UserRepository) GetByEmail(ctx context.Context, email string) (*domain.User, error) {
query := `SELECT id, name, email, password, created_at, updated_at FROM users WHERE email = $1`
user := &domain.User{}
err := r.db.QueryRowContext(ctx, query, email).Scan(
&user.ID, &user.Name, &user.Email, &user.Password,
&user.CreatedAt, &user.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return user, nil
}
Handler HTTP
// internal/delivery/http/user_handler.go
package http
import (
"encoding/json"
"net/http"
"myapp/internal/usecase"
)
type UserHandler struct {
userUseCase *usecase.UserUseCase
}
func NewUserHandler(uc *usecase.UserUseCase) *UserHandler {
return &UserHandler{userUseCase: uc}
}
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
var input usecase.CreateUserInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "JSON inválido")
return
}
user, err := h.userUseCase.Create(r.Context(), input)
if err != nil {
handleError(w, err)
return
}
respondJSON(w, http.StatusCreated, user)
}
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
user, err := h.userUseCase.GetByID(r.Context(), id)
if err != nil {
handleError(w, err)
return
}
respondJSON(w, http.StatusOK, user)
}
Injeção de Dependências
// cmd/api/main.go
package main
import (
"database/sql"
"log"
"net/http"
_ "github.com/lib/pq"
"myapp/internal/delivery/http"
"myapp/internal/repository/postgres"
"myapp/internal/usecase"
"myapp/pkg/hasher"
)
func main() {
// Conexão com banco de dados
db, err := sql.Open("postgres", "postgres://localhost/myapp?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Repositórios
userRepo := postgres.NewUserRepository(db)
// Serviços
passwordHasher := hasher.NewBcryptHasher()
// Casos de uso
userUseCase := usecase.NewUserUseCase(userRepo, passwordHasher)
// Handlers
userHandler := http.NewUserHandler(userUseCase)
// Rotas
mux := http.NewServeMux()
mux.HandleFunc("POST /api/users", userHandler.Create)
mux.HandleFunc("GET /api/users/{id}", userHandler.Get)
log.Println("Servidor iniciando na porta 8080...")
http.ListenAndServe(":8080", mux)
}
Benefícios
- Testabilidade: Cada camada pode ser testada isoladamente
- Flexibilidade: Trocar banco de dados ou framework é simples
- Manutenibilidade: Código organizado e fácil de entender
- Escalabilidade: Fácil adicionar novas funcionalidades
- Clareza: Separação clara de responsabilidades
Quando Usar
Clean Architecture é ideal para:
- Aplicações de médio a grande porte
- Sistemas que precisam de longevidade
- Equipes múltiplas trabalhando no mesmo código
- Sistemas com requisitos de teste rigorosos
Para aplicações pequenas ou MVPs, a complexidade adicional pode não valer a pena.