Clean architecture em Go: organizando por camadas
1. Introdução à Clean Architecture no Contexto Go
A Clean Architecture, proposta por Robert C. Martin, encontra em Go um terreno fértil para sua implementação. A simplicidade da linguagem, seu sistema de pacotes bem definido e as interfaces implícitas criam um ambiente natural para aplicar os princípios de separação por camadas.
Em Go, não precisamos de frameworks complexos para implementar inversão de dependência. Uma interface com dois métodos e um construtor que recebe essa interface já estabelece o contrato necessário. Essa simplicidade reduz o boilerplate e mantém o foco no que realmente importa: as regras de negócio.
Os princípios fundamentais adaptados ao ecossistema Go são:
- Entidades (camada mais interna): structs de domínio sem dependências externas
- Casos de Uso: orquestração de regras de negócio com interfaces definidas pelo domínio
- Adaptadores de Interface: conversão entre o mundo externo e o domínio
- Frameworks & Drivers: configurações de infraestrutura na periferia
A regra de ouro: dependências apontam para dentro. Camadas internas nunca sabem sobre camadas externas.
2. Camada de Entidades (Entities / Domain)
As entidades são o coração do sistema. Em Go, representamos entidades como structs com métodos de negócio. Esta camada não importa nada — zero dependências externas.
package domain
import (
"errors"
"time"
)
type User struct {
ID string
Name string
Email string
CreatedAt time.Time
}
func NewUser(name, email string) (*User, error) {
if name == "" {
return nil, errors.New("name cannot be empty")
}
if email == "" {
return nil, errors.New("email cannot be empty")
}
return &User{
ID: generateUUID(),
Name: name,
Email: email,
CreatedAt: time.Now(),
}, nil
}
func (u *User) UpdateEmail(newEmail string) error {
if newEmail == "" {
return errors.New("email cannot be empty")
}
u.Email = newEmail
return nil
}
package domain
import (
"errors"
"time"
)
type OrderStatus string
const (
OrderPending OrderStatus = "pending"
OrderConfirmed OrderStatus = "confirmed"
OrderShipped OrderStatus = "shipped"
)
type Order struct {
ID string
UserID string
Items []OrderItem
Status OrderStatus
Total float64
CreatedAt time.Time
}
type OrderItem struct {
ProductID string
Quantity int
Price float64
}
func NewOrder(userID string, items []OrderItem) (*Order, error) {
if userID == "" {
return nil, errors.New("user ID is required")
}
if len(items) == 0 {
return nil, errors.New("order must have at least one item")
}
var total float64
for _, item := range items {
if item.Quantity <= 0 {
return nil, errors.New("item quantity must be positive")
}
total += item.Price * float64(item.Quantity)
}
return &Order{
ID: generateUUID(),
UserID: userID,
Items: items,
Status: OrderPending,
Total: total,
CreatedAt: time.Now(),
}, nil
}
func (o *Order) Confirm() error {
if o.Status != OrderPending {
return errors.New("only pending orders can be confirmed")
}
o.Status = OrderConfirmed
return nil
}
3. Camada de Casos de Uso (Use Cases / Application)
Os casos de uso orquestram as regras de negócio. Eles definem interfaces que serão implementadas pelos adaptadores, mantendo o domínio puro.
package application
import (
"context"
"your-project/domain"
)
// Interfaces definidas pelo domínio
type UserRepository interface {
Save(ctx context.Context, user *domain.User) error
FindByID(ctx context.Context, id string) (*domain.User, error)
FindByEmail(ctx context.Context, email string) (*domain.User, error)
}
type OrderRepository interface {
Save(ctx context.Context, order *domain.Order) error
FindByUserID(ctx context.Context, userID string) ([]*domain.Order, error)
}
// DTOs de entrada e saída
type CreateUserInput struct {
Name string
Email string
}
type CreateUserOutput struct {
ID string
Name string
Email string
}
// Caso de uso
type CreateUserUseCase struct {
userRepo UserRepository
}
func NewCreateUserUseCase(userRepo UserRepository) *CreateUserUseCase {
return &CreateUserUseCase{userRepo: userRepo}
}
func (uc *CreateUserUseCase) Execute(ctx context.Context, input CreateUserInput) (*CreateUserOutput, error) {
// Verificar se email já existe
existing, _ := uc.userRepo.FindByEmail(ctx, input.Email)
if existing != nil {
return nil, ErrEmailAlreadyExists
}
// Criar entidade de domínio
user, err := domain.NewUser(input.Name, input.Email)
if err != nil {
return nil, err
}
// Persistir
if err := uc.userRepo.Save(ctx, user); err != nil {
return nil, err
}
return &CreateUserOutput{
ID: user.ID,
Name: user.Name,
Email: user.Email,
}, nil
}
package application
import (
"context"
"your-project/domain"
)
type ListOrdersInput struct {
UserID string
}
type ListOrdersOutput struct {
Orders []OrderOutput
}
type OrderOutput struct {
ID string
Status string
Total float64
CreatedAt string
}
type ListOrdersUseCase struct {
orderRepo OrderRepository
}
func NewListOrdersUseCase(orderRepo OrderRepository) *ListOrdersUseCase {
return &ListOrdersUseCase{orderRepo: orderRepo}
}
func (uc *ListOrdersUseCase) Execute(ctx context.Context, input ListOrdersInput) (*ListOrdersOutput, error) {
orders, err := uc.orderRepo.FindByUserID(ctx, input.UserID)
if err != nil {
return nil, err
}
var output ListOrdersOutput
for _, order := range orders {
output.Orders = append(output.Orders, OrderOutput{
ID: order.ID,
Status: string(order.Status),
Total: order.Total,
CreatedAt: order.CreatedAt.Format("2006-01-02T15:04:05Z"),
})
}
return &output, nil
}
4. Camada de Adaptadores de Interface (Interface Adapters)
Os adaptadores convertem dados entre o mundo externo e o domínio. Handlers HTTP recebem requisições e chamam casos de uso. Repositórios concretos implementam as interfaces definidas pelo domínio.
package handlers
import (
"encoding/json"
"net/http"
"your-project/application"
)
type UserHandler struct {
createUserUseCase *application.CreateUserUseCase
listOrdersUseCase *application.ListOrdersUseCase
}
func NewUserHandler(
createUser *application.CreateUserUseCase,
listOrders *application.ListOrdersUseCase,
) *UserHandler {
return &UserHandler{
createUserUseCase: createUser,
listOrdersUseCase: listOrders,
}
}
func (h *UserHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
var input application.CreateUserInput
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
http.Error(w, "invalid request body", http.StatusBadRequest)
return
}
output, err := h.createUserUseCase.Execute(r.Context(), input)
if err != nil {
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(output)
}
package repositories
import (
"context"
"database/sql"
"your-project/application"
"your-project/domain"
)
type PostgresUserRepository struct {
db *sql.DB
}
func NewPostgresUserRepository(db *sql.DB) *PostgresUserRepository {
return &PostgresUserRepository{db: db}
}
func (r *PostgresUserRepository) Save(ctx context.Context, user *domain.User) error {
query := `INSERT INTO users (id, name, email, created_at) VALUES ($1, $2, $3, $4)`
_, err := r.db.ExecContext(ctx, query, user.ID, user.Name, user.Email, user.CreatedAt)
return err
}
func (r *PostgresUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) {
query := `SELECT id, name, email, created_at FROM users WHERE id = $1`
row := r.db.QueryRowContext(ctx, query, id)
var user domain.User
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err != nil {
return nil, err
}
return &user, nil
}
func (r *PostgresUserRepository) FindByEmail(ctx context.Context, email string) (*domain.User, error) {
query := `SELECT id, name, email, created_at FROM users WHERE email = $1`
row := r.db.QueryRowContext(ctx, query, email)
var user domain.User
err := row.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
if err != nil {
return nil, err
}
return &user, nil
}
5. Camada de Frameworks e Drivers (Infrastructure)
Esta é a camada mais externa, onde configuramos banco de dados, servidores HTTP e frameworks. Aqui aplicamos a inversão de dependência: injetamos implementações concretas nas abstrações definidas pelo domínio.
package main
import (
"database/sql"
"log"
"net/http"
"your-project/application"
"your-project/handlers"
"your-project/repositories"
)
func main() {
// Configurar banco de dados
db, err := sql.Open("postgres", "postgres://user:password@localhost/mydb?sslmode=disable")
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Instanciar repositórios (adaptadores de saída)
userRepo := repositories.NewPostgresUserRepository(db)
orderRepo := repositories.NewPostgresOrderRepository(db)
// Instanciar casos de uso
createUserUseCase := application.NewCreateUserUseCase(userRepo)
listOrdersUseCase := application.NewListOrdersUseCase(orderRepo)
// Instanciar handlers (adaptadores de entrada)
userHandler := handlers.NewUserHandler(createUserUseCase, listOrdersUseCase)
// Configurar rotas HTTP
mux := http.NewServeMux()
mux.HandleFunc("POST /users", userHandler.CreateUser)
mux.HandleFunc("GET /users/{id}/orders", userHandler.ListOrders)
// Iniciar servidor
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
6. Fluxo de Requisição Através das Camadas
Quando uma requisição HTTP chega, o fluxo percorre as camadas de fora para dentro e depois retorna:
[HTTP Request]
↓
[Handler] (Interface Adapter) - Converte JSON para DTO
↓
[Use Case] (Application) - Orquestra regras de negócio
↓
[Repository Interface] (definida no domínio)
↓
[PostgresRepository] (Interface Adapter) - Acessa banco
↓
[Database] (Framework/Driver)
Cada camada tem responsabilidades claras e pode ser testada isoladamente. Os handlers testam serialização/deserialização. Os casos de uso testam lógica de negócio com mocks de repositórios. Os repositórios testam queries com testcontainers.
Dependências apontam para dentro:
Frameworks → Interface Adapters → Application → Domain
(infra) (handlers, repos) (use cases) (entities)
7. Testes e Manutenibilidade
A organização por camadas torna os testes muito mais simples. Casos de uso podem ser testados com mocks das interfaces de repositório:
func TestCreateUserUseCase(t *testing.T) {
mockRepo := new(MockUserRepository)
mockRepo.On("FindByEmail", mock.Anything, "test@test.com").Return(nil, nil)
mockRepo.On("Save", mock.Anything, mock.Anything).Return(nil)
useCase := application.NewCreateUserUseCase(mockRepo)
input := application.CreateUserInput{Name: "John", Email: "test@test.com"}
output, err := useCase.Execute(context.Background(), input)
assert.NoError(t, err)
assert.NotEmpty(t, output.ID)
assert.Equal(t, "John", output.Name)
}
Os benefícios são claros:
- Testabilidade: cada camada é testável independentemente
- Manutenibilidade: mudar de banco de dados? Só trocar o repositório
- Evolução: adicionar novos casos de uso sem impactar camadas existentes
- Clareza: a estrutura do código reflete a arquitetura
A Clean Architecture em Go não é sobre ferramentas ou frameworks — é sobre disciplina na organização do código. Comece pequeno, mantenha as camadas separadas e veja seu sistema crescer de forma sustentável.
Referências
- The Clean Architecture (Robert C. Martin) — Artigo seminal de Uncle Bob definindo os princípios da Clean Architecture
- Go by Example: Interfaces — Documentação oficial sobre interfaces implícitas em Go, fundamentais para a inversão de dependência
- Standard Go Project Layout — Layout de projeto Go padrão que organiza código por camadas
- Practical Go: Real world advice for writing maintainable Go programs — Palestra de Dave Cheney sobre organização de código Go, incluindo separação de responsabilidades
- Testing with Testcontainers for Go — Guia oficial para testes de integração com containers, útil para testar adaptadores de saída
- Uber Go Style Guide — Guia de estilo da Uber para Go, com boas práticas de organização de pacotes e interfaces
- Go with Clean Architecture: A Practical Example — Artigo prático implementando Clean Architecture em Go com exemplos reais