Go

Clean Architecture em Go: Organizando Projetos para Escalar: Do Básico ao Avançado

12 min de leitura

Clean Architecture em Go: Organizando Projetos para Escalar: Do Básico ao Avançado

O que é Clean Architecture Clean Architecture é um conjunto de princípios de design que visa criar sistemas de software independentes de frameworks, testáveis, independentes de interface de usuário, independentes de banco de dados e independentes de qualquer agente externo. Proposto por Robert C. Martin (Uncle Bob), o padrão organiza o código em camadas concêntricas, onde as dependências sempre apontam para dentro. Cada camada tem responsabilidades bem definidas, e a camada mais interna (domínio) nunca conhece a camada mais externa (frameworks e drivers). Em Go, a implementação de Clean Architecture ganha características próprias da linguagem, como a preferência por interfaces implícitas e composição sobre herança. O objetivo prático é criar projetos que crescem sem dificuldade, onde modificar uma dependência externa (como trocar de banco de dados) não quebra toda a lógica de negócio. Isso economiza tempo de manutenção e facilita testes automatizados significativamente. Por que Clean Architecture importa em Go Go é uma linguagem excelente para backend, mas projetos sem

<h2>O que é Clean Architecture</h2>

<p>Clean Architecture é um conjunto de princípios de design que visa criar sistemas de software independentes de frameworks, testáveis, independentes de interface de usuário, independentes de banco de dados e independentes de qualquer agente externo. Proposto por Robert C. Martin (Uncle Bob), o padrão organiza o código em camadas concêntricas, onde as dependências sempre apontam para dentro. Cada camada tem responsabilidades bem definidas, e a camada mais interna (domínio) nunca conhece a camada mais externa (frameworks e drivers).</p>

<p>Em Go, a implementação de Clean Architecture ganha características próprias da linguagem, como a preferência por interfaces implícitas e composição sobre herança. O objetivo prático é criar projetos que crescem sem dificuldade, onde modificar uma dependência externa (como trocar de banco de dados) não quebra toda a lógica de negócio. Isso economiza tempo de manutenção e facilita testes automatizados significativamente.</p>

<h3>Por que Clean Architecture importa em Go</h3>

<p>Go é uma linguagem excelente para backend, mas projetos sem estrutura clara viram um &quot;spaghetti code&quot; rapidamente. Clean Architecture força decisões de design que Go naturalmente incentiva: simplicidade, clareza e separação de responsabilidades. Um projeto bem estruturado em Go é altamente manutenível porque a linguagem tem pouca &quot;magia&quot; — tudo é explícito.</p>

<h2>As Camadas da Clean Architecture</h2>

<p>A Clean Architecture divide-se em 4 camadas principais, cada uma com responsabilidades específicas. O fluxo de dependências sempre aponta para dentro, nunca sai da esfera central.</p>

<h3>Camada de Domínio (Entities)</h3>

<p>Esta é a camada mais interna e contém as entidades do negócio — a lógica que não mudaria nem se você trocar de web framework ou banco de dados. São estruturas simples de dados e funções que implementam regras de negócio fundamentais. Essa camada não deve importar nada de fora dela.</p>

<pre><code class="language-go">// domain/user.go

package domain

import &quot;errors&quot;

// User representa a entidade de usuário

type User struct {

ID string

Name string

Email string

Age int

}

// NewUser cria um novo usuário com validações de negócio

func NewUser(id, name, email string, age int) (*User, error) {

if name == &quot;&quot; {

return nil, errors.New(&quot;nome é obrigatório&quot;)

}

if age &lt; 18 {

return nil, errors.New(&quot;usuário deve ser maior de 18 anos&quot;)

}

if !isValidEmail(email) {

return nil, errors.New(&quot;email inválido&quot;)

}

return &amp;User{

ID: id,

Name: name,

Email: email,

Age: age,

}, nil

}

func isValidEmail(email string) bool {

// Validação simples

return len(email) &gt; 5 &amp;&amp; len(email) &lt; 254

}</code></pre>

<h3>Camada de Casos de Uso (Use Cases)</h3>

<p>Aqui vivem os interatores (use cases) que orquestram a lógica de negócio. Um caso de uso representa uma ação específica do sistema — por exemplo, &quot;registrar novo usuário&quot; ou &quot;atualizar perfil&quot;. Essa camada conhece a camada de domínio, mas não conhece detalhes de implementação como HTTP ou banco de dados.</p>

<pre><code class="language-go">// usecase/register_user.go

package usecase

import (

&quot;context&quot;

&quot;github.com/seu-usuario/seu-projeto/domain&quot;

)

// UserRepository define o contrato para persistência

type UserRepository interface {

Save(ctx context.Context, user *domain.User) error

FindByEmail(ctx context.Context, email string) (*domain.User, error)

}

// RegisterUserUseCase implementa o caso de uso de registro

type RegisterUserUseCase struct {

userRepo UserRepository

}

func NewRegisterUserUseCase(repo UserRepository) *RegisterUserUseCase {

return &amp;RegisterUserUseCase{userRepo: repo}

}

// Execute executa o caso de uso

func (u *RegisterUserUseCase) Execute(ctx context.Context,

id, name, email string, age int) (*domain.User, error) {

// Verifica se usuário já existe

existing, _ := u.userRepo.FindByEmail(ctx, email)

if existing != nil {

return nil, ErrUserAlreadyExists

}

// Cria a entidade (validações de domínio acontecem aqui)

user, err := domain.NewUser(id, name, email, age)

if err != nil {

return nil, err

}

// Persiste o usuário

if err := u.userRepo.Save(ctx, user); err != nil {

return nil, err

}

return user, nil

}

var ErrUserAlreadyExists = domain.NewDomainError(&quot;usuário com este email já existe&quot;)</code></pre>

<h3>Camada de Interface de Adaptadores (Adapters)</h3>

<p>Essa camada contém os adaptadores que fazem a conversão entre o mundo externo (HTTP, gRPC, filas) e os casos de uso. Aqui vivem os controllers, presenters, gateways e implementações de repositórios. A camada conhece casos de uso mas não é conhecida por eles (inversão de controle).</p>

<pre><code class="language-go">// adapter/http/handler.go

package http

import (

&quot;encoding/json&quot;

&quot;net/http&quot;

&quot;github.com/seu-usuario/seu-projeto/usecase&quot;

)

type RegisterUserHandler struct {

registerUseCase *usecase.RegisterUserUseCase

}

func NewRegisterUserHandler(useCase usecase.RegisterUserUseCase) RegisterUserHandler {

return &amp;RegisterUserHandler{registerUseCase: useCase}

}

type RegisterUserRequest struct {

ID string json:&quot;id&quot;

Name string json:&quot;name&quot;

Email string json:&quot;email&quot;

Age int json:&quot;age&quot;

}

type UserResponse struct {

ID string json:&quot;id&quot;

Name string json:&quot;name&quot;

Email string json:&quot;email&quot;

Age int json:&quot;age&quot;

}

func (h RegisterUserHandler) Handle(w http.ResponseWriter, r http.Request) {

var req RegisterUserRequest

if err := json.NewDecoder(r.Body).Decode(&amp;req); err != nil {

w.WriteHeader(http.StatusBadRequest)

return

}

user, err := h.registerUseCase.Execute(r.Context(),

req.ID, req.Name, req.Email, req.Age)

if err != nil {

w.WriteHeader(http.StatusUnprocessableEntity)

json.NewEncoder(w).Encode(map[string]string{&quot;error&quot;: err.Error()})

return

}

response := UserResponse{

ID: user.ID,

Name: user.Name,

Email: user.Email,

Age: user.Age,

}

w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)

w.WriteHeader(http.StatusCreated)

json.NewEncoder(w).Encode(response)

}</code></pre>

<h3>Camada de Frameworks e Drivers (Frameworks &amp; DB)</h3>

<p>A camada mais externa contém implementações específicas de banco de dados, frameworks web, logging, e outras dependências externas. Essa é a camada que muda mais frequentemente e deve ser a mais isolada possível do resto do código.</p>

<pre><code class="language-go">// adapter/storage/postgres_user_repository.go

package storage

import (

&quot;context&quot;

&quot;database/sql&quot;

&quot;github.com/seu-usuario/seu-projeto/domain&quot;

_ &quot;github.com/lib/pq&quot;

)

type PostgresUserRepository struct {

db *sql.DB

}

func NewPostgresUserRepository(db sql.DB) PostgresUserRepository {

return &amp;PostgresUserRepository{db: db}

}

func (r PostgresUserRepository) Save(ctx context.Context, user domain.User) error {

query := INSERT INTO users (id, name, email, age) VALUES ($1, $2, $3, $4)

_, err := r.db.ExecContext(ctx, query,

user.ID, user.Name, user.Email, user.Age)

return err

}

func (r *PostgresUserRepository) FindByEmail(ctx context.Context,

email string) (*domain.User, error) {

query := SELECT id, name, email, age FROM users WHERE email = $1

var user domain.User

err := r.db.QueryRowContext(ctx, query, email).Scan(

&amp;user.ID, &amp;user.Name, &amp;user.Email, &amp;user.Age)

if err == sql.ErrNoRows {

return nil, nil

}

if err != nil {

return nil, err

}

return &amp;user, nil

}</code></pre>

<h2>Estrutura de Diretórios e Wiring</h2>

<p>A organização física do projeto reflete as camadas lógicas. Um projeto bem estruturado em Go segue um padrão claro de diretórios, facilitando navegação e manutenção.</p>

<h3>Estrutura de Pastas Recomendada</h3>

<pre><code>seu-projeto/

├── cmd/

│ └── main.go # Entry point da aplicação

├── domain/ # Entidades e lógica pura

│ ├── user.go

│ └── error.go

├── usecase/ # Casos de uso / Interatores

│ ├── register_user.go

│ └── get_user.go

├── adapter/

│ ├── http/ # Controllers HTTP

│ │ ├── handler.go

│ │ └── middleware.go

│ └── storage/ # Implementações de repositórios

│ ├── postgres_user_repository.go

│ └── memory_user_repository.go

├── infra/ # Configuração de DI e setup

│ └── wire.go

├── go.mod

└── go.sum</code></pre>

<h3>Inversão de Controle com Wire</h3>

<p>Go não possui containers de DI nativos. A biblioteca <code>wire</code> do Google resolve isso gerando código de injeção de dependência em tempo de compilação.</p>

<pre><code class="language-go">// infra/wire.go

// +build wireinject

package infra

import (

&quot;database/sql&quot;

&quot;github.com/google/wire&quot;

&quot;github.com/seu-usuario/seu-projeto/adapter/http&quot;

&quot;github.com/seu-usuario/seu-projeto/adapter/storage&quot;

&quot;github.com/seu-usuario/seu-projeto/usecase&quot;

)

func InitializeHandler(db sql.DB) http.RegisterUserHandler {

wire.Build(

storage.NewPostgresUserRepository,

usecase.NewRegisterUserUseCase,

http.NewRegisterUserHandler,

)

return nil // Wire gera a implementação real

}</code></pre>

<p>Execute <code>wire</code> no diretório para gerar o código: <code>wire ./infra/...</code></p>

<h2>Testabilidade e Benefícios Práticos</h2>

<p>Uma das maiores vantagens da Clean Architecture é a facilidade de testes. Como cada camada tem responsabilidades claras e usa interfaces, criar mocks é trivial.</p>

<h3>Testando Casos de Uso</h3>

<pre><code class="language-go">// usecase/register_user_test.go

package usecase

import (

&quot;context&quot;

&quot;testing&quot;

&quot;github.com/seu-usuario/seu-projeto/domain&quot;

)

// MockUserRepository implementa UserRepository para testes

type MockUserRepository struct {

SaveCalled bool

Users map[string]*domain.User

}

func (m MockUserRepository) Save(ctx context.Context, user domain.User) error {

m.SaveCalled = true

m.Users[user.Email] = user

return nil

}

func (m MockUserRepository) FindByEmail(ctx context.Context, email string) (domain.User, error) {

return m.Users[email], nil

}

func TestRegisterUserUseCase_Success(t *testing.T) {

mockRepo := &amp;MockUserRepository{Users: make(map[string]*domain.User)}

useCase := NewRegisterUserUseCase(mockRepo)

user, err := useCase.Execute(context.Background(),

&quot;123&quot;, &quot;João&quot;, &quot;joao@example.com&quot;, 25)

if err != nil {

t.Fatalf(&quot;esperava sucesso, got %v&quot;, err)

}

if user.Name != &quot;João&quot; {

t.Errorf(&quot;esperava João, got %s&quot;, user.Name)

}

if !mockRepo.SaveCalled {

t.Error(&quot;esperava que Save fosse chamado&quot;)

}

}

func TestRegisterUserUseCase_DuplicateEmail(t *testing.T) {

mockRepo := &amp;MockUserRepository{

Users: map[string]*domain.User{

&quot;joao@example.com&quot;: {ID: &quot;456&quot;, Name: &quot;João Antigo&quot;},

},

}

useCase := NewRegisterUserUseCase(mockRepo)

_, err := useCase.Execute(context.Background(),

&quot;123&quot;, &quot;João Novo&quot;, &quot;joao@example.com&quot;, 25)

if err != ErrUserAlreadyExists {

t.Errorf(&quot;esperava ErrUserAlreadyExists, got %v&quot;, err)

}

}</code></pre>

<h3>Trocar de Banco de Dados é Trivial</h3>

<p>Se precisar trocar PostgreSQL por MongoDB, você só muda a implementação do repositório. Os casos de uso não sabem disso e nem o código HTTP.</p>

<pre><code class="language-go">// adapter/storage/mongo_user_repository.go

package storage

import (

&quot;context&quot;

&quot;go.mongodb.org/mongo-driver/mongo&quot;

&quot;github.com/seu-usuario/seu-projeto/domain&quot;

)

type MongoUserRepository struct {

collection *mongo.Collection

}

func NewMongoUserRepository(collection mongo.Collection) MongoUserRepository {

return &amp;MongoUserRepository{collection: collection}

}

func (r MongoUserRepository) Save(ctx context.Context, user domain.User) error {

_, err := r.collection.InsertOne(ctx, user)

return err

}

func (r MongoUserRepository) FindByEmail(ctx context.Context, email string) (domain.User, error) {

var user domain.User

err := r.collection.FindOne(ctx, map[string]string{&quot;email&quot;: email}).Decode(&amp;user)

if err == mongo.ErrNoDocuments {

return nil, nil

}

return &amp;user, err

}</code></pre>

<p>Agora na injeção de dependência, você só troca qual implementação usar. O resto do código não muda.</p>

<h2>Conclusão</h2>

<p>Clean Architecture em Go fornece uma estrutura sólida para projetos que precisam crescer. Os três pontos principais aprendidos são: (1) A separação em camadas concêntricas com dependências apontando para dentro garante que a lógica de negócio é independente de detalhes técnicos; (2) O uso de interfaces e inversão de controle torna testes automatizados simples e rápidos, porque substituir implementações reais por mocks é natural; (3) A estrutura de diretórios clara reflete as responsabilidades, facilitando onboarding de novos desenvolvedores e reduzindo tempo de manutenção quando mudanças externas (como trocar banco de dados) são necessárias.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html" target="_blank" rel="noopener noreferrer">The Clean Architecture - Robert C. Martin</a></li>

<li><a href="https://github.com/google/wire" target="_blank" rel="noopener noreferrer">Google Wire - Dependency Injection for Go</a></li>

<li><a href="https://github.com/marcusoldham/go-clean-architecture" target="_blank" rel="noopener noreferrer">Clean Architecture in Go - Marcus Olsson</a></li>

<li><a href="https://www.golang-book.com/" target="_blank" rel="noopener noreferrer">Domain-Driven Design in Go - Matthew Boyle</a></li>

<li><a href="https://golang.org/doc/effective_go" target="_blank" rel="noopener noreferrer">Go Code Review Comments - Effective Go</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Boas Práticas de Fuzzing em Go: Testes Baseados em Propriedades com go test -fuzz para Times Ágeis
Boas Práticas de Fuzzing em Go: Testes Baseados em Propriedades com go test -fuzz para Times Ágeis

O que é Fuzzing e por que você deveria se importar Fuzzing é uma técnica de t...

O que Todo Dev Deve Saber sobre SQLC em Go: Gerando Código Tipado a partir de Queries SQL
O que Todo Dev Deve Saber sobre SQLC em Go: Gerando Código Tipado a partir de Queries SQL

O que é SQLC e Por que Você Deveria Usar SQLC é uma ferramenta que gera códig...

O que Todo Dev Deve Saber sobre Pacote os e filepath em Go: Sistema de Arquivos e Ambiente
O que Todo Dev Deve Saber sobre Pacote os e filepath em Go: Sistema de Arquivos e Ambiente

Entendendo o Pacote em Go O pacote é fundamental para qualquer programa Go qu...