Golang

Artigo 44 — Clean Architecture em Go: camadas e dependências Já leu

14 min de leitura

Artigo 44 — Clean Architecture em Go: camadas e dependências
Artigo 44 — Clean Architecture em Go: camadas e dependências Por que arquitetura importa Um projeto que começa com vinte arquivos e cresce para duzentos sem

Artigo 44 — Clean Architecture em Go: camadas e dependências

Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 8 — Arquitetura e Padrões


Por que arquitetura importa

Um projeto que começa com vinte arquivos e cresce para duzentos sem uma organização deliberada torna-se progressivamente mais difícil de modificar. Mudanças em uma parte quebram outra parte inesperada. Testes exigem o banco de dados inteiro rodando. Trocar a biblioteca HTTP exige modificar metade do código.

Clean Architecture — popularizada por Robert C. Martin — resolve esses problemas com um princípio central: as dependências de código só podem apontar para dentro. Camadas internas não conhecem camadas externas. O domínio do negócio não sabe que existe um banco de dados, um servidor HTTP ou um arquivo de configuração.


As camadas

┌─────────────────────────────────────┐
│         Frameworks & Drivers        │  HTTP, banco de dados, CLI
├─────────────────────────────────────┤
│        Interface Adapters           │  Handlers, repositórios, presenters
├─────────────────────────────────────┤
│          Use Cases                  │  Regras de negócio da aplicação
├─────────────────────────────────────┤
│            Entities                 │  Regras de negócio do domínio
└─────────────────────────────────────┘
           ↑ dependências apontam para dentro

Em Go, essa estrutura se traduz em pacotes:

loja/
├── cmd/
│   └── api/
│       └── main.go              ← ponto de entrada
├── internal/
│   ├── domain/                  ← entidades e regras de domínio
│   │   ├── produto.go
│   │   ├── pedido.go
│   │   └── usuario.go
│   ├── usecase/                 ← casos de uso
│   │   ├── produto/
│   │   │   ├── criar.go
│   │   │   ├── buscar.go
│   │   │   └── listar.go
│   │   └── pedido/
│   │       ├── criar.go
│   │       └── processar.go
│   ├── repository/              ← interfaces de repositório
│   │   ├── produto.go
│   │   └── pedido.go
│   ├── handler/                 ← adaptadores HTTP
│   │   ├── produto.go
│   │   └── pedido.go
│   └── infra/                   ← implementações de infraestrutura
│       ├── postgres/
│       │   ├── produto_repo.go
│       │   └── pedido_repo.go
│       └── redis/
│           └── cache.go
└── pkg/
    └── validator/

Camada de domínio: entidades e regras

A camada de domínio não importa nada de fora do projeto. Ela define tipos, erros e regras de negócio puras:

// internal/domain/produto.go
package domain

import (
    "errors"
    "time"
)

// Erros de domínio — regras de negócio violadas
var (
    ErrNomeProdutoVazio    = errors.New("nome do produto não pode ser vazio")
    ErrPrecoProdutoInvalido = errors.New("preço do produto deve ser maior que zero")
    ErrEstoqueNegativo     = errors.New("estoque não pode ser negativo")
    ErrEstoqueInsuficiente = errors.New("estoque insuficiente para esta operação")
)

// Produto representa um produto no catálogo da loja.
type Produto struct {
    id          int64
    nome        string
    descricao   string
    preco       float64
    estoque     int
    categoriaID int64
    ativo       bool
    criadoEm   time.Time
}

// NovoProduto cria um Produto validado.
// Retorna erro se qualquer regra de negócio for violada.
func NovoProduto(nome, descricao string, preco float64, categoriaID int64) (*Produto, error) {
    if nome == "" {
        return nil, ErrNomeProdutoVazio
    }
    if preco <= 0 {
        return nil, ErrPrecoProdutoInvalido
    }

    return &Produto{
        nome:        nome,
        descricao:   descricao,
        preco:       preco,
        categoriaID: categoriaID,
        ativo:       true,
        criadoEm:   time.Now().UTC(),
    }, nil
}

// Getters — acesso controlado ao estado interno
func (p *Produto) ID() int64          { return p.id }
func (p *Produto) Nome() string       { return p.nome }
func (p *Produto) Descricao() string  { return p.descricao }
func (p *Produto) Preco() float64     { return p.preco }
func (p *Produto) Estoque() int       { return p.estoque }
func (p *Produto) CategoriaID() int64 { return p.categoriaID }
func (p *Produto) Ativo() bool        { return p.ativo }
func (p *Produto) CriadoEm() time.Time { return p.criadoEm }

// Regras de negócio como métodos do domínio

// AtualizarPreco altera o preço do produto.
// O novo preço deve ser maior que zero.
func (p *Produto) AtualizarPreco(novoPreco float64) error {
    if novoPreco <= 0 {
        return ErrPrecoProdutoInvalido
    }
    p.preco = novoPreco
    return nil
}

// AdicionarEstoque incrementa o estoque do produto.
func (p *Produto) AdicionarEstoque(quantidade int) error {
    if quantidade <= 0 {
        return errors.New("quantidade deve ser maior que zero")
    }
    p.estoque += quantidade
    return nil
}

// RetirarEstoque decrementa o estoque.
// Retorna ErrEstoqueInsuficiente se não houver estoque suficiente.
func (p *Produto) RetirarEstoque(quantidade int) error {
    if quantidade <= 0 {
        return errors.New("quantidade deve ser maior que zero")
    }
    if p.estoque < quantidade {
        return ErrEstoqueInsuficiente
    }
    p.estoque -= quantidade
    return nil
}

// Desativar marca o produto como inativo.
func (p *Produto) Desativar() {
    p.ativo = false
}
// internal/domain/pedido.go
package domain

import (
    "errors"
    "time"
)

var (
    ErrPedidoVazio          = errors.New("pedido não pode ter zero itens")
    ErrStatusInvalido       = errors.New("transição de status inválida")
    ErrPedidoJaFinalizado   = errors.New("pedido já foi finalizado")
)

type StatusPedido string

const (
    StatusPendente   StatusPedido = "pendente"
    StatusConfirmado StatusPedido = "confirmado"
    StatusEnviado    StatusPedido = "enviado"
    StatusEntregue   StatusPedido = "entregue"
    StatusCancelado  StatusPedido = "cancelado"
)

type ItemPedido struct {
    ProdutoID  int64
    Nome       string
    Quantidade int
    PrecoUnit  float64
}

func (i ItemPedido) Subtotal() float64 {
    return float64(i.Quantidade) * i.PrecoUnit
}

type Pedido struct {
    id         int64
    usuarioID  int64
    itens      []ItemPedido
    status     StatusPedido
    total      float64
    criadoEm  time.Time
}

func NovoPedido(usuarioID int64, itens []ItemPedido) (*Pedido, error) {
    if len(itens) == 0 {
        return nil, ErrPedidoVazio
    }

    total := 0.0
    for _, item := range itens {
        total += item.Subtotal()
    }

    return &Pedido{
        usuarioID: usuarioID,
        itens:     itens,
        status:    StatusPendente,
        total:     total,
        criadoEm: time.Now().UTC(),
    }, nil
}

func (p *Pedido) ID() int64              { return p.id }
func (p *Pedido) UsuarioID() int64       { return p.usuarioID }
func (p *Pedido) Itens() []ItemPedido    { return p.itens }
func (p *Pedido) Status() StatusPedido   { return p.status }
func (p *Pedido) Total() float64         { return p.total }
func (p *Pedido) CriadoEm() time.Time    { return p.criadoEm }

// Confirmar transiciona o pedido de Pendente para Confirmado.
func (p *Pedido) Confirmar() error {
    if p.status != StatusPendente {
        return fmt.Errorf("%w: %s → confirmado", ErrStatusInvalido, p.status)
    }
    p.status = StatusConfirmado
    return nil
}

// Cancelar cancela o pedido se ainda não foi enviado.
func (p *Pedido) Cancelar() error {
    if p.status == StatusEnviado || p.status == StatusEntregue {
        return fmt.Errorf("%w: não é possível cancelar pedido %s", ErrPedidoJaFinalizado, p.status)
    }
    p.status = StatusCancelado
    return nil
}

Interfaces de repositório: contratos sem implementação

// internal/repository/produto.go
package repository

import (
    "context"

    "loja/internal/domain"
)

// ProdutoRepository define o contrato para persistência de produtos.
// Implementações concretas ficam em internal/infra.
type ProdutoRepository interface {
    Criar(ctx context.Context, produto *domain.Produto) error
    BuscarPorID(ctx context.Context, id int64) (*domain.Produto, error)
    Listar(ctx context.Context, filtro ProdutoFiltro) ([]*domain.Produto, error)
    Atualizar(ctx context.Context, produto *domain.Produto) error
    Deletar(ctx context.Context, id int64) error
}

type ProdutoFiltro struct {
    CategoriaID *int64
    ApenasAtivo bool
    Limite      int
    Offset      int
}

// PedidoRepository define o contrato para persistência de pedidos.
type PedidoRepository interface {
    Criar(ctx context.Context, pedido *domain.Pedido) error
    BuscarPorID(ctx context.Context, id int64) (*domain.Pedido, error)
    ListarPorUsuario(ctx context.Context, usuarioID int64) ([]*domain.Pedido, error)
    Atualizar(ctx context.Context, pedido *domain.Pedido) error
}

Casos de uso: orquestrando o domínio

// internal/usecase/produto/criar.go
package produto

import (
    "context"
    "fmt"

    "loja/internal/domain"
    "loja/internal/repository"
)

type CriarProdutoInput struct {
    Nome        string
    Descricao   string
    Preco       float64
    CategoriaID int64
}

type CriarProdutoOutput struct {
    ID          int64
    Nome        string
    Preco       float64
    CriadoEm   string
}

type CriarProdutoUseCase struct {
    repo repository.ProdutoRepository
}

func NovoCriarProdutoUseCase(repo repository.ProdutoRepository) *CriarProdutoUseCase {
    return &CriarProdutoUseCase{repo: repo}
}

func (uc *CriarProdutoUseCase) Executar(ctx context.Context, input CriarProdutoInput) (*CriarProdutoOutput, error) {
    // 1. Cria entidade de domínio — valida regras de negócio
    produto, err := domain.NovoProduto(
        input.Nome,
        input.Descricao,
        input.Preco,
        input.CategoriaID,
    )
    if err != nil {
        return nil, fmt.Errorf("criar produto: %w", err)
    }

    // 2. Persiste através do repositório
    if err := uc.repo.Criar(ctx, produto); err != nil {
        return nil, fmt.Errorf("persistir produto: %w", err)
    }

    // 3. Retorna output
    return &CriarProdutoOutput{
        ID:        produto.ID(),
        Nome:      produto.Nome(),
        Preco:     produto.Preco(),
        CriadoEm: produto.CriadoEm().Format(time.RFC3339),
    }, nil
}
// internal/usecase/pedido/criar.go
package pedido

import (
    "context"
    "fmt"

    "loja/internal/domain"
    "loja/internal/repository"
)

type ItemInput struct {
    ProdutoID  int64
    Quantidade int
}

type CriarPedidoInput struct {
    UsuarioID int64
    Itens     []ItemInput
}

type CriarPedidoOutput struct {
    PedidoID int64
    Total    float64
    Status   string
}

type CriarPedidoUseCase struct {
    pedidoRepo  repository.PedidoRepository
    produtoRepo repository.ProdutoRepository
}

func NovoCriarPedidoUseCase(
    pedidoRepo repository.PedidoRepository,
    produtoRepo repository.ProdutoRepository,
) *CriarPedidoUseCase {
    return &CriarPedidoUseCase{
        pedidoRepo:  pedidoRepo,
        produtoRepo: produtoRepo,
    }
}

func (uc *CriarPedidoUseCase) Executar(ctx context.Context, input CriarPedidoInput) (*CriarPedidoOutput, error) {
    // Resolve produtos e valida disponibilidade
    var itensPedido []domain.ItemPedido
    for _, itemInput := range input.Itens {
        produto, err := uc.produtoRepo.BuscarPorID(ctx, itemInput.ProdutoID)
        if err != nil {
            return nil, fmt.Errorf("produto %d: %w", itemInput.ProdutoID, err)
        }

        if !produto.Ativo() {
            return nil, fmt.Errorf("produto %d está inativo", itemInput.ProdutoID)
        }

        // Regra de domínio: verifica estoque
        if err := produto.RetirarEstoque(itemInput.Quantidade); err != nil {
            return nil, fmt.Errorf("produto %s: %w", produto.Nome(), err)
        }

        // Atualiza estoque no repositório
        if err := uc.produtoRepo.Atualizar(ctx, produto); err != nil {
            return nil, fmt.Errorf("atualizar estoque: %w", err)
        }

        itensPedido = append(itensPedido, domain.ItemPedido{
            ProdutoID:  produto.ID(),
            Nome:       produto.Nome(),
            Quantidade: itemInput.Quantidade,
            PrecoUnit:  produto.Preco(),
        })
    }

    // Cria o pedido no domínio
    pedido, err := domain.NovoPedido(input.UsuarioID, itensPedido)
    if err != nil {
        return nil, err
    }

    // Confirma automaticamente
    if err := pedido.Confirmar(); err != nil {
        return nil, err
    }

    // Persiste
    if err := uc.pedidoRepo.Criar(ctx, pedido); err != nil {
        return nil, fmt.Errorf("persistir pedido: %w", err)
    }

    return &CriarPedidoOutput{
        PedidoID: pedido.ID(),
        Total:    pedido.Total(),
        Status:   string(pedido.Status()),
    }, nil
}

Implementação de repositório: infraestrutura

// internal/infra/postgres/produto_repo.go
package postgres

import (
    "context"
    "database/sql"
    "fmt"
    "time"

    "loja/internal/domain"
    "loja/internal/repository"
)

type produtoRepo struct {
    db *sql.DB
}

// NewProdutoRepository retorna uma implementação PostgreSQL de ProdutoRepository.
func NewProdutoRepository(db *sql.DB) repository.ProdutoRepository {
    return &produtoRepo{db: db}
}

func (r *produtoRepo) Criar(ctx context.Context, p *domain.Produto) error {
    query := `
        INSERT INTO produtos (nome, descricao, preco, estoque, categoria_id, ativo, criado_em)
        VALUES ($1, $2, $3, $4, $5, $6, $7)
        RETURNING id
    `
    var id int64
    err := r.db.QueryRowContext(ctx, query,
        p.Nome(), p.Descricao(), p.Preco(),
        p.Estoque(), p.CategoriaID(), p.Ativo(), p.CriadoEm(),
    ).Scan(&id)
    if err != nil {
        return fmt.Errorf("inserir produto: %w", err)
    }

    // Injeta o ID gerado pelo banco — necessário porque o domínio
    // não conhece a estratégia de geração de IDs
    setarID(p, id)
    return nil
}

// setarID usa um mecanismo para injetar o ID no domínio sem exportar o campo
// Em projetos reais, use um método SetID no domínio ou um construtor de
// reconstituição separado do construtor de criação
func setarID(p *domain.Produto, id int64) {
    // Solução pragmática: método de reconstituição no domínio
    p.SetID(id)
}

func (r *produtoRepo) BuscarPorID(ctx context.Context, id int64) (*domain.Produto, error) {
    query := `
        SELECT id, nome, descricao, preco, estoque, categoria_id, ativo, criado_em
        FROM produtos
        WHERE id = $1 AND deletado_em IS NULL
    `

    var (
        pid         int64
        nome        string
        descricao   string
        preco       float64
        estoque     int
        categoriaID int64
        ativo       bool
        criadoEm   time.Time
    )

    err := r.db.QueryRowContext(ctx, query, id).Scan(
        &pid, &nome, &descricao, &preco,
        &estoque, &categoriaID, &ativo, &criadoEm,
    )

    if err == sql.ErrNoRows {
        return nil, fmt.Errorf("produto %d: %w", id, domain.ErrProdutoNaoEncontrado)
    }
    if err != nil {
        return nil, fmt.Errorf("buscar produto: %w", err)
    }

    // Reconstitui a entidade a partir dos dados persistidos
    return domain.ReconstituirProduto(pid, nome, descricao, preco, estoque, categoriaID, ativo, criadoEm), nil
}

Handler HTTP: adaptador de entrada

// internal/handler/produto.go
package handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "strconv"

    "github.com/go-chi/chi/v5"

    "loja/internal/domain"
    usecaseproduto "loja/internal/usecase/produto"
)

type ProdutoHandler struct {
    criar  *usecaseproduto.CriarProdutoUseCase
    buscar *usecaseproduto.BuscarProdutoUseCase
    listar *usecaseproduto.ListarProdutosUseCase
}

func NovoProdutoHandler(
    criar *usecaseproduto.CriarProdutoUseCase,
    buscar *usecaseproduto.BuscarProdutoUseCase,
    listar *usecaseproduto.ListarProdutosUseCase,
) *ProdutoHandler {
    return &ProdutoHandler{criar: criar, buscar: buscar, listar: listar}
}

func (h *ProdutoHandler) Rotas() func(chi.Router) {
    return func(r chi.Router) {
        r.Get("/", h.Listar)
        r.Post("/", h.Criar)
        r.Get("/{id}", h.Buscar)
    }
}

type criarProdutoRequest struct {
    Nome        string  `json:"nome"`
    Descricao   string  `json:"descricao"`
    Preco       float64 `json:"preco"`
    CategoriaID int64   `json:"categoria_id"`
}

func (h *ProdutoHandler) Criar(w http.ResponseWriter, r *http.Request) {
    var req criarProdutoRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        responderErro(w, http.StatusBadRequest, "corpo da requisição inválido")
        return
    }

    output, err := h.criar.Executar(r.Context(), usecaseproduto.CriarProdutoInput{
        Nome:        req.Nome,
        Descricao:   req.Descricao,
        Preco:       req.Preco,
        CategoriaID: req.CategoriaID,
    })

    if err != nil {
        // Traduz erros de domínio para status HTTP
        switch {
        case errors.Is(err, domain.ErrNomeProdutoVazio),
             errors.Is(err, domain.ErrPrecoProdutoInvalido):
            responderErro(w, http.StatusUnprocessableEntity, err.Error())
        default:
            responderErro(w, http.StatusInternalServerError, "erro interno")
        }
        return
    }

    responderJSON(w, http.StatusCreated, output)
}

func (h *ProdutoHandler) Buscar(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
    if err != nil {
        responderErro(w, http.StatusBadRequest, "id inválido")
        return
    }

    output, err := h.buscar.Executar(r.Context(), id)
    if err != nil {
        if errors.Is(err, domain.ErrProdutoNaoEncontrado) {
            responderErro(w, http.StatusNotFound, "produto não encontrado")
            return
        }
        responderErro(w, http.StatusInternalServerError, "erro interno")
        return
    }

    responderJSON(w, http.StatusOK, output)
}

func (h *ProdutoHandler) Listar(w http.ResponseWriter, r *http.Request) {
    output, err := h.listar.Executar(r.Context())
    if err != nil {
        responderErro(w, http.StatusInternalServerError, "erro interno")
        return
    }
    responderJSON(w, http.StatusOK, output)
}

func responderJSON(w http.ResponseWriter, status int, dados any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(dados)
}

func responderErro(w http.ResponseWriter, status int, mensagem string) {
    responderJSON(w, status, map[string]string{"erro": mensagem})
}

Composição no main: injeção de dependências

// cmd/api/main.go
package main

import (
    "database/sql"
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
    _ "github.com/lib/pq"

    "loja/internal/handler"
    "loja/internal/infra/postgres"
    usecasepedido "loja/internal/usecase/pedido"
    usecaseproduto "loja/internal/usecase/produto"
)

func main() {
    // 1. Infraestrutura
    db, err := sql.Open("postgres",
        "postgres://usuario:senha@localhost:5432/loja?sslmode=disable")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 2. Repositórios (infraestrutura)
    produtoRepo := postgres.NewProdutoRepository(db)
    pedidoRepo := postgres.NewPedidoRepository(db)

    // 3. Casos de uso (lógica de negócio)
    criarProduto := usecaseproduto.NovoCriarProdutoUseCase(produtoRepo)
    buscarProduto := usecaseproduto.NovoBuscarProdutoUseCase(produtoRepo)
    listarProdutos := usecaseproduto.NovoListarProdutosUseCase(produtoRepo)
    criarPedido := usecasepedido.NovoCriarPedidoUseCase(pedidoRepo, produtoRepo)

    // 4. Handlers (adaptadores de entrada)
    produtoHandler := handler.NovoProdutoHandler(criarProduto, buscarProduto, listarProdutos)
    pedidoHandler := handler.NovoPedidoHandler(criarPedido)

    // 5. Roteamento
    r := chi.NewRouter()
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    r.Use(middleware.Timeout(30 * time.Second))

    r.Route("/api/v1", func(r chi.Router) {
        r.Route("/produtos", produtoHandler.Rotas())
        r.Route("/pedidos", pedidoHandler.Rotas())
    })

    // 6. Servidor
    fmt.Println("API em :8080")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Testando casos de uso com mocks

// internal/usecase/produto/criar_test.go
package produto_test

import (
    "context"
    "errors"
    "testing"

    "loja/internal/domain"
    "loja/internal/usecase/produto"
)

// Mock do repositório
type mockProdutoRepo struct {
    criarFn func(ctx context.Context, p *domain.Produto) error
}

func (m *mockProdutoRepo) Criar(ctx context.Context, p *domain.Produto) error {
    return m.criarFn(ctx, p)
}

func (m *mockProdutoRepo) BuscarPorID(_ context.Context, _ int64) (*domain.Produto, error) {
    return nil, nil
}

func (m *mockProdutoRepo) Listar(_ context.Context, _ repository.ProdutoFiltro) ([]*domain.Produto, error) {
    return nil, nil
}

func (m *mockProdutoRepo) Atualizar(_ context.Context, _ *domain.Produto) error { return nil }
func (m *mockProdutoRepo) Deletar(_ context.Context, _ int64) error              { return nil }

func TestCriarProduto_Sucesso(t *testing.T) {
    repo := &mockProdutoRepo{
        criarFn: func(_ context.Context, _ *domain.Produto) error {
            return nil // simula persistência bem-sucedida
        },
    }

    uc := produto.NovoCriarProdutoUseCase(repo)
    output, err := uc.Executar(context.Background(), produto.CriarProdutoInput{
        Nome:        "Notebook",
        Preco:       4500.00,
        CategoriaID: 1,
    })

    if err != nil {
        t.Fatalf("esperava sucesso, obteve: %v", err)
    }
    if output.Nome != "Notebook" {
        t.Errorf("nome esperado 'Notebook', obtido '%s'", output.Nome)
    }
}

func TestCriarProduto_NomeVazio(t *testing.T) {
    repo := &mockProdutoRepo{}
    uc := produto.NovoCriarProdutoUseCase(repo)

    _, err := uc.Executar(context.Background(), produto.CriarProdutoInput{
        Nome:  "", // inválido
        Preco: 100.0,
    })

    if !errors.Is(err, domain.ErrNomeProdutoVazio) {
        t.Errorf("esperava ErrNomeProdutoVazio, obteve: %v", err)
    }
}

Resumo do que foi coberto

Este artigo apresentou Clean Architecture em Go de forma completa: a estrutura de camadas com dependências apontando para dentro, a camada de domínio com entidades encapsuladas e regras de negócio como métodos, interfaces de repositório como contratos sem implementação, casos de uso orquestrando o domínio, implementações de repositório em PostgreSQL, handlers HTTP como adaptadores de entrada, composição no main.go com injeção de dependências e testes de casos de uso com mocks simples. O próximo artigo explora Domain-Driven Design aplicado a projetos Go.


Referências e leituras complementares

  • Clean Architecture — Robert C. Martin — O livro original que define a arquitetura. https://www.amazon.com/Clean-Architecture-Craftsmans-Software-Structure/dp/0134494164

  • Go Clean Architecture — exemplo no GitHub — Implementação de referência da comunidade. https://github.com/bxcodec/go-clean-arch

  • Screaming Architecture — Artigo de Uncle Bob sobre arquitetura que "grita" o domínio. https://blog.cleancoder.com/uncle-bob/2011/09/30/Screaming-Architecture.html

  • Dependency Injection in Go — Técnicas de injeção de dependências sem framework. https://go.dev/wiki/DependencyInjection

  • Wire — Google — Gerador de código para injeção de dependências em Go. https://github.com/google/wire

  • Standard Go Project Layout — Estrutura de projeto amplamente adotada. https://github.com/golang-standards/project-layout


Próximo artigo: Artigo 45 — Domain-Driven Design aplicado a projetos Go**


you asked

Sim


claude response

Comentários

Mais em Golang

Artigo 32 — Introdução ao database/sql e drivers em Go
Artigo 32 — Introdução ao database/sql e drivers em Go

Artigo 32 — Introdução ao database/sql e drivers em Go A camada de abstração...

Criando ferramentas de linha de comando com flag e Cobra
Criando ferramentas de linha de comando com flag e Cobra

Go é a linguagem de fato para ferramentas de linha de comando. Docker, Kubern...

Select, timeouts e cancelamento de operações
Select, timeouts e cancelamento de operações

O artigo anterior introduziu o select como mecanismo para aguardar múltiplos...