Esvaziando a Mochila Com a Linguagem Go

Principais Pontos
  • Go promove simplicidade radical ao eliminar abstrações desnecessárias, resultando em código mais legível e manutenível que cresce linearmente com a complexidade
  • Composição em vez de herança e wiring explícito eliminam a magia de frameworks, tornando o fluxo de dados transparente e o debugging trivial
  • Funções como valores de primeira ordem substituem naturalmente muitos padrões de design complexos como Strategy, Decorator e Command
  • Testes de integração com containers reais oferecem mais confiança que pirâmides de testes unitários isolados com mocks pesados
  • Cada simplificação tem trade-offs claros: menos abstrações significam mais código manual, mas também maior clareza e controle sobre o sistema

Esta semana eu li uma postagem do Sibelius Seraphini sobre sua determinação em banir a complexidade no desenvolvimento de software na Woovi. Esta postagem ressalta um sentimento crescente na comunidade de desenvolvimento: a fadiga com a complexidade desnecessária. O Sibelius listou conceitos que eram considerados "boas práticas" inquestionáveis há alguns anos, mas que muitas vezes criam mais problemas do que resolvem, e sem dúvida adicionam bastante complexidade:

Print da postagem do Sibelius - Banindo complexidade

Alerta de Opinião
Antes que você feche este artigo pensando que é mais um manifesto extremista, deixe-me esclarecer: eu não defendo banir conceitos e tecnologias. Para mim tecnologias são ferramentas, e cada contexto tem suas necessidades. O que proponho neste artigo é algo diferente: quero mostrar como a linguagem Go, por design, oferece alternativas pragmáticas para quem busca simplicidade sem sacrificar qualidade, escalabilidade e experiência do desenvolvedor. E, sim, existem outras linguagens que também atendem a este requisito, e pode ser que no futuro eu escreva uma continuação deste artigo. Quem sabe...

Este artigo não é sobre trocar Java por Elixir ou abraçar programação funcional pura. É sobre como Go — uma linguagem imperativa e "boring" — consegue entregar software de qualidade com uma fração da cerimônia que aceitamos como normal.


Por que Go favorece o minimalismo

Go nasceu para resolver um problema cultural na Google: compilar rápido, ser fácil de ler em code‑review e rodar em produção sem dores. Para atingir esses objetivos, a linguagem entrega:

  • Tipagem estática com inferência modesta — você ganha segurança de tipo sem precisar decorar generics complexos.
  • Biblioteca padrão poderosa — HTTP, JSON, context, crypto, concurrency; metade do que em outras linguagens é "framework" o Go já traz de fábrica.
  • Ferramentas de build e testes integradasgo test, go vet, go fmt, go mod e agora govulncheck. O ecossistema incentiva pipelines simples.
  • Runtime de concorrência leve — goroutines custam bytes; canais compõem padrões de comunicação sem lock.

Quando a linguagem entrega o essencial, o risco de buscar "frameworks salvadores" diminui. Menos camadas, menos mocks, menos reuniões sobre "qual padrão usar".

Insight

"Menos camadas, menos mocks, menos reuniões sobre 'qual padrão usar'."


Você não precisa de…

PARADIGMAS DE PROGRAMAÇÃO

1. OOP — Composição > Herança

A orientação a objetos nasceu para modelar mundos complexos com hierarquias e polimorfismo. Em Go, a sintaxe já reforça outra mentalidade: composição > herança.

// Exemplo: domínio de cobrança usando imutabilidade
type Invoice struct {
    ID     int
    Amount int64 // sempre em centavos para evitar float
    Paid   bool
}

// Por que passar por valor e não *Invoice?
// 1. Structs pequenas (<256 bytes) são copiadas eficientemente
// 2. Imutabilidade = zero race conditions em goroutines
// 3. Rollback trivial: basta não usar o retorno
func Pay(inv Invoice) (Invoice, error) {
    // Invoice original permanece intacta - seguro para concorrência
    if inv.Paid {
        return Invoice{}, fmt.Errorf("já paga")
    }
    
    // Modifica a cópia local, não o original
    inv.Paid = true
    return inv, nil // retorna nova versão
}

Não existe nenhuma herança implícita. Se precisar de polimorfismo, faça via interfaces pequenas e concisas que descrevem comportamento.

Vantagens
  • Redução significativa no código: Consolidação de múltiplas classes hierárquicas em estruturas mais simples e diretas
  • Refatoração mais eficiente: Mudanças localizadas sem propagação através de hierarquias complexas
  • Curva de aprendizado reduzida: Estrutura mais linear facilita compreensão do domínio
  • Eliminação de problemas clássicos de herança: Sem diamond problem ou ambiguidades de métodos virtuais
Trade-offs
  • Adaptação necessária: Desenvolvedores com experiência em OOP precisam ajustar abordagem
  • Gerenciamento manual de código comum: Ausência de herança requer estratégias diferentes para reutilização
  • Suporte limitado de ferramentas: Menos recursos automáticos para refatoração e visualização de estruturas
  • Cuidados com embedding: Promoção de campos pode gerar conflitos que exigem atenção

2. Padrões Gang of Four — Funções como passageiros de primeira classe

Os 23 padrões do GoF nasceram em 1994 para resolver limitações de C++ e Smalltalk. Em Go, muitos deles são desnecessários porque a linguagem já oferece estruturas primitivas muito poderosas (apesar de simples).

Por exemplo:

// Strategy Pattern: função como parâmetro substitui hierarquia de classes
func ProcessPayment(amount int64, strategy func(int64) error) error {
    // Em Java: PaymentStrategy interface + N implementações
    // Em Go: passa a função diretamente
    return strategy(amount)
}

// Uso: ProcessPayment(100, processWithStripe)
//      ProcessPayment(100, processWithPayPal)

// Decorator Pattern: higher-order function em vez de herança
func WithRetry(fn func() error) func() error {
    return func() error {
        // Tenta 3x com backoff exponencial
        for i := 0; i < 3; i++ {
            if err := fn(); err == nil {
                return nil // sucesso, sai do loop
            }
            // Espera 1s, 2s, 3s entre tentativas
            time.Sleep(time.Second * time.Duration(i+1))
        }
        return fn() // última tentativa
    }
}

// Observer Pattern: canais substituem callbacks/listeners
type EventBus struct {
    // map[tópico] → lista de canais interessados
    subscribers map[string][]chan Event
    mu          sync.RWMutex // proteção para map concurrent
}

func (e *EventBus) Subscribe(topic string) <-chan Event {
    ch := make(chan Event, 10) // buffer evita bloqueio do publisher
    e.mu.Lock()
    e.subscribers[topic] = append(e.subscribers[topic], ch)
    e.mu.Unlock()
    return ch // retorna canal read-only pro subscriber
}
GoF Pattern Necessidade em Go Alternativa Idiomática
Singleton Raro sync.Once + global var
Factory Desnecessário Funções construtoras
Builder Ocasional Functional options
Adapter Desnecessário Interfaces implícitas
Bridge Nunca Composição
Composite Raro Structs recursivas
Proxy Desnecessário Interface + wrapper
Command Desnecessário Funções + closures
Iterator Built-in range loops
Mediator Raro Canais
Memento Simples Copy struct
State Ocasional Type switches
Template Desnecessário Funções + embedding
Visitor Complexo Type assertions
Vantagens
  • Implementação mais concisa: Padrões complexos substituídos por funções como valores
  • Localização facilitada: Busca textual simples identifica todos os usos rapidamente
  • Performance otimizada: Eliminação de indireções e possibilidade de otimizações do compilador
  • Testes simplificados: Substituição direta de funções sem necessidade de frameworks complexos
Trade-offs
  • Reinterpretação de padrões estabelecidos: Conceitos clássicos de design patterns requerem nova abordagem
  • Limitações de ferramentas de navegação: Recursos de IDE menos efetivos para rastrear implementações funcionais
  • Debugging de closures: Pontos de parada em funções anônimas podem ser menos intuitivos
  • Ausência de metaprogramação: Aspectos transversais implementados manualmente

3. Decorators — Higher-order functions

Decorators em linguagens como Python/Java adicionam comportamento via anotações. Go usa composição e higher-order functions:

// Decorator pattern clássico em Go
type Handler func(http.ResponseWriter, *http.Request)

// Decorator de autenticação
func WithAuth(h Handler) Handler {
    return func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "unauthorized", 401)
            return
        }
        h(w, r)
    }
}

// Decorator de logging
func WithLogging(h Handler) Handler {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        h(w, r)
        log.Printf("%s %s took %v", r.Method, r.URL.Path, time.Since(start))
    }
}

// Decorator de rate limiting usando higher-order function
func WithRateLimit(limit int) func(Handler) Handler {
    // limiter é criado UMA vez e "capturado" pela closure
    // Em Java/C# seria uma variável de instância em uma classe
    limiter := rate.NewLimiter(rate.Limit(limit), limit)
    
    // Retorna função que "lembra" do limiter via closure
    return func(h Handler) Handler {
        return func(w http.ResponseWriter, r *http.Request) {
            // limiter persiste entre requisições - estado compartilhado
            // sem precisar de classes ou singletons
            if !limiter.Allow() {
                http.Error(w, "rate limit exceeded", 429)
                return
            }
            h(w, r) // chama o próximo handler na cadeia
        }
    }
}

// Composição explícita: cada camada é visível e testável
func main() {
    // Lê-se de dentro pra fora: payment → rate limit → auth → logging
    // Em Java seriam 4 classes com herança ou proxies dinâmicos
    handler := WithLogging(
        WithAuth(
            WithRateLimit(100)( // 100 req/s por instância
                handlePayment,  // handler original sem decoração
            ),
        ),
    )
    
    // Stack trace mostra exatamente a ordem de execução
    http.HandleFunc("/payment", handler)
}
Vantagens
  • Stack traces mais claros: Cadeia de decorators visível e rastreável
  • Testes modulares: Cada decorator pode ser testado independentemente
  • Sem overhead de reflexão: Composição resolvida em tempo de compilação
  • Fluxo explícito: Ordem de execução clara diretamente no código
Trade-offs
  • Complexidade com múltiplas camadas: Composição manual pode ficar menos legível com muitos decorators
  • Ausência de anotações: Comportamentos transversais devem ser explicitamente codificados
  • Ordenação manual: Desenvolvedor responsável por definir sequência correta
  • Potencial duplicação: Padrões similares podem se repetir entre handlers

ARQUITETURA E DESIGN

4. Containers de Injeção de Dependência — Wiring explícito

Em muitos ecossistemas, DI resolve acoplamento cíclico e fabricação de objetos caros. Go simplifica ao explicitar dependências no construtor ou na função:

// Injeção manual: dependências são parâmetros explícitos
func makePayHandler(store InvoiceStore) http.HandlerFunc {
    // Closure captura store - sem necessidade de campo struct
    return func(w http.ResponseWriter, r *http.Request) {
        // store disponível via closure, não via this.store
        invoice, _ := store.FindByID(r.Context(), "123")
        // … processa pagamento …
    }
}

func main() {
    // Wiring manual: você vê cada dependência sendo criada
    db, _ := sql.Open("postgres", os.Getenv("PG_CONN"))
    store := pgStore{db: db}
    
    // Composição explícita - sem @Autowired mágico
    http.Handle("/pay", makePayHandler(store))
    
    // Árvore de dependências visível no main()
    log.Fatal(http.ListenAndServe(":8080", nil))
}
Vantagens
  • Rastreabilidade de dependências: Mudanças nas dependências claramente visíveis em revisões de código
  • Inicialização mais rápida: Tempo de startup reduzido sem necessidade de scanning e criação de proxies
  • Debugging direto: Fluxo de criação de objetos facilmente rastreável
  • Busca simplificada: Localização de dependências através de busca textual simples
Trade-offs
  • Escalabilidade do wiring manual: Complexidade aumenta com o número de componentes
  • Configuração inicial extensa: Projetos grandes podem ter configuração inicial substancial
  • Gerenciamento manual de ciclo de vida: Responsabilidade de gerenciar ordem de inicialização e finalização
  • Limitações reconhecidas: Ferramentas como Wire foram criadas para auxiliar em projetos muito grandes

5. Clean Architecture — Organização pragmática

Clean Architecture promete independência de frameworks e testabilidade total. Na prática, muitos projetos acabam com uma explosão de abstrações que obscurecem a lógica de negócio:

// Abordagem "Clean" típica - 7 arquivos para uma operação simples
// user/domain/entities/user.go
type User struct { ID, Name string }

// user/domain/repositories/user_repository.go
type UserRepository interface {
    FindByID(context.Context, string) (*User, error)
}

// user/application/usecases/get_user.go
type GetUserUseCase struct {
    repo UserRepository
}

// user/infrastructure/persistence/postgres_user_repository.go
type PostgresUserRepository struct { db *sql.DB }

// user/interfaces/http/user_controller.go
type UserController struct {
    useCase *GetUserUseCase
}

// user/interfaces/http/dto/user_dto.go
type UserDTO struct { ID, Name string }

// main.go - wiring de 50 linhas

Compare com a abordagem pragmática em Go:

// users/users.go - domínio e interface no mesmo lugar
package users

type User struct {
    ID   string
    Name string
}

// Interface declarada onde é usada, não onde é implementada
// Filosofia Go: "accept interfaces, return structs"
type Store interface {
    FindByID(ctx context.Context, id string) (*User, error)
}

// GetByID aceita qualquer tipo que implemente Store
// Não precisa declarar "implements Store" - duck typing implícito
func GetByID(ctx context.Context, store Store, id string) (*User, error) {
    // Regra de negócio fica no domínio, não no repositório
    if id == "" {
        return nil, errors.New("id required")
    }
    
    // Store pode ser postgres, mock, redis... não importa
    return store.FindByID(ctx, id)
}

// users/postgres.go - implementação concreta
type postgresStore struct { db *sql.DB }

// Retorna Store (interface), não *postgresStore (concreto)
// Isso desacopla consumidores da implementação específica
func NewPostgresStore(db *sql.DB) Store {
    return &postgresStore{db: db}
}

// FindByID existe? postgresStore implementa Store automaticamente!
// Sem "implements", sem hierarquia, sem acoplamento
func (p *postgresStore) FindByID(ctx context.Context, id string) (*User, error) {
    // implementação...
}

// cmd/api/main.go - wiring explícito sem mágica
db := connectDB()
userStore := users.NewPostgresStore(db) // Store interface
http.HandleFunc("/users/", makeUserHandler(userStore))

Estrutura de pastas resultante:

myapp/
├── users/          # tudo sobre usuários
│   ├── users.go    # tipos e regras
│   └── postgres.go # implementação do Store
├── billing/        # tudo sobre cobrança
├── shipping/       # tudo sobre entrega
└── cmd/
    └── api/        # entry point
Vantagens
  • Aprendizado mais rápido: Estrutura por domínio facilita compreensão para novos membros
  • Navegação simplificada: Organização por funcionalidade em vez de por camada técnica
  • Testes próximos ao código: Testes localizados junto com a implementação
  • Menos arquivos: Estrutura mais enxuta comparada a implementações em múltiplas camadas
Trade-offs
  • Mudança de paradigma arquitetural: Pode haver questionamentos sobre a ausência de camadas tradicionais
  • Necessidade de disciplina: Prevenção de imports circulares depende de convenções da equipe
  • Troca de persistência mais acoplada: Mudanças de banco de dados podem requerer mais alterações
  • Divergência de padrões estabelecidos: Arquiteturas tradicionais precisam ser reinterpretadas

6. Clean Code — Clareza > cerimônia

O livro Clean Code popularizou práticas como "funções devem fazer uma coisa só" e "nomes devem ser expressivos". Embora válidas, muitas equipes transformaram essas diretrizes em dogma quase religioso:

// "Clean Code" levado ao pé da letra
type UserCreationService struct {
    validator UserValidator
    repository UserRepository
    notifier NotificationService
    logger Logger
}

func (s *UserCreationService) CreateUser(dto UserCreationDTO) (*User, error) {
    validatedDTO, err := s.validator.ValidateUserCreationDTO(dto)
    if err != nil {
        return nil, s.wrapErrorWithContext(err, "validation failed")
    }
    
    user := s.mapDTOToUser(validatedDTO)
    savedUser, err := s.repository.SaveUser(user)
    if err != nil {
        return nil, s.wrapErrorWithContext(err, "save failed")
    }
    
    s.notifier.NotifyUserCreated(savedUser)
    s.logger.LogUserCreation(savedUser)
    
    return savedUser, nil
}

// 15 métodos auxiliares de 3 linhas cada...

Compare com código idiomático Go:

// Go pragmático: clareza em vez de cerimônia
func CreateUser(db *sql.DB, name, email string) (int64, error) {
    // Validação inline quando simples
    if name == "" || email == "" {
        return 0, errors.New("name and email required")
    }
    
    // Operação direta
    result, err := db.Exec(
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
        name, email,
    )
    if err != nil {
        return 0, fmt.Errorf("create user: %w", err)
    }
    
    return result.LastInsertId()
}
Vantagens
  • Stack traces mais concisos: Menos níveis de indireção para rastrear
  • Debugging simplificado: Lógica concentrada facilita identificação de problemas
  • Revisões de código mais eficientes: Menos arquivos e indireções para analisar
  • Redução de mudanças de contexto: Lógica relacionada mantida próxima
Trade-offs
  • Princípio de responsabilidade única flexibilizado: Funções podem ter múltiplas responsabilidades relacionadas
  • Testes potencialmente mais abrangentes: Funções maiores podem requerer cenários de teste mais complexos
  • Métricas de cobertura diferentes: Menos granularidade nas métricas de teste
  • Refatoração menos granular: Mudanças podem afetar blocos maiores de código

7. Service/Controller/Repository — Handlers diretos

O padrão MVC e suas variações criam camadas artificiais que muitas vezes apenas movem dados de um lado para outro:

// Padrão tradicional: 4 arquivos para um CRUD
// controller/user_controller.go
type UserController struct {
    service *UserService
}

func (c *UserController) GetUser(id string) (*UserDTO, error) {
    user, err := c.service.GetUser(id)
    if err != nil {
        return nil, err
    }
    return mapToDTO(user), nil
}

// service/user_service.go
type UserService struct {
    repo UserRepository
}

func (s *UserService) GetUser(id string) (*User, error) {
    // Literalmente só repassa a chamada
    return s.repo.FindByID(id)
}

// repository/user_repository.go
type UserRepository interface {
    FindByID(id string) (*User, error)
}

// dto/user_dto.go
type UserDTO struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

Abordagem Go: handlers que fazem o trabalho:

// users.go - tudo junto e misturado (de propósito!)
type UserStore interface {
    GetUser(ctx context.Context, id string) (*User, error)
}

func MakeUserHandler(store UserStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        id := chi.URLParam(r, "id")
        
        user, err := store.GetUser(r.Context(), id)
        if err != nil {
            http.Error(w, err.Error(), http.StatusNotFound)
            return
        }
        
        json.NewEncoder(w).Encode(user)
    }
}
Vantagens
  • Redução substancial de indireção: Fluxo direto do request ao armazenamento
  • Rastreabilidade simplificada: Busca textual simples localiza todo o fluxo
  • Testes mais diretos: Menos camadas para configurar e mockar
  • Menos código boilerplate: Implementação mais concisa sem camadas intermediárias
Trade-offs
  • Adaptação para desenvolvedores de outros frameworks: Ausência de anotações e camadas familiares
  • Implementação manual de aspectos: Funcionalidades transversais devem ser explicitamente adicionadas
  • Lógica de negócio próxima ao transporte: Menor separação entre camadas
  • Reuso entre protocolos: Pode requerer refatoração para suportar múltiplos tipos de API

PRÁTICAS DE DESENVOLVIMENTO

8. Mutabilidade Compartilhada — Canais e imutabilidade

Em linguagens tradicionais, objetos mutáveis compartilhados entre threads são fonte inesgotável de bugs: race conditions, deadlocks, estado inconsistente. Go abraça a filosofia "Don't communicate by sharing memory; share memory by communicating".

Copiar structs pequenos é barato — o compilador otimiza agressivamente. Para dados maiores, use canais para coordenar mudanças:

// Estado imutável - cada operação retorna nova cópia
type Account struct {
    Balance int64
    Locked  bool
}

func Withdraw(acc Account, amount int64) (Account, error) {
    if acc.Locked {
        return Account{}, errors.New("conta bloqueada")
    }
    if acc.Balance < amount {
        return Account{}, errors.New("saldo insuficiente")
    }
    acc.Balance -= amount
    return acc, nil
}

// Coordenação via canal - "share memory by communicating"
type Bank struct {
    // Canal único = fila de operações = zero race conditions
    accounts chan accountOp
}

type accountOp struct {
    id     string
    amount int64
    result chan error // resposta assíncrona pro cliente
}

// Goroutine única gerencia todo estado - actor pattern
func (b *Bank) processOps() {
    // Estado privado da goroutine - ninguém mais acessa
    state := make(map[string]Account)
    
    // Loop infinito processando uma operação por vez
    for op := range b.accounts {
        // Busca conta atual (zero value se não existe)
        acc := state[op.id]
        
        // Aplica operação imutável
        newAcc, err := Withdraw(acc, op.amount)
        if err == nil {
            state[op.id] = newAcc // atualiza estado local
        }
        
        // Responde pro cliente via canal
        op.result <- err
    }
}
Vantagens
  • Redução drástica de bugs de concorrência: Race conditions minimizadas através de isolamento
  • Rollback simplificado: Erros não afetam estado anterior
  • Comportamento mais previsível: Fluxo de execução determinístico
  • Escalabilidade eficiente: Modelo de concorrência leve permite muitas goroutines
Trade-offs
  • Impacto em memória: Cópias frequentes de estruturas grandes aumentam alocações
  • Maior atividade do garbage collector: Padrão de muitas alocações pequenas
  • Complexidade com múltiplos canais: Coordenação pode se tornar difícil de gerenciar
  • Considerações de latência: Cópias podem impactar caminhos críticos de performance

9. Testes Unitários vs Testes de integração — Equilíbrio pragmático

Há valor em ambas as abordagens. Testes unitários são excelentes para lógica de negócio complexa:

// Teste unitário valioso - testa lógica de negócio pura
func TestCalculateDiscount(t *testing.T) {
    cases := []struct {
        amount   int64
        userType string
        expected int64
    }{
        {1000, "regular", 900},    // 10% desconto
        {1000, "premium", 800},    // 20% desconto
        {500, "regular", 500},     // sem desconto abaixo do mínimo
    }
    
    for _, tc := range cases {
        result := CalculateDiscount(tc.amount, tc.userType)
        assert.Equal(t, tc.expected, result)
    }
}

// Mock excessivo - evite para código trivial
type MockUserRepo struct {
    mock.Mock
}
// ... 20 linhas de mock para testar 3 linhas de código

Para fluxos completos, prefira testes de integração com testcontainers:

// Teste E2E com banco real - confiança máxima, zero mocks
func TestUserAPI(t *testing.T) {
    ctx := context.Background()
    
    // Container Postgres sobe em ~500ms (primeira vez ~2s com download)
    // Alpine = imagem mínima, startup rápido
    postgres, _ := postgres.RunContainer(ctx,
        testcontainers.WithImage("postgres:15-alpine"),
        postgres.WithDatabase("test"),
        postgres.WithPassword("test"),
        testcontainers.WithWaitStrategy(
            // Espera log específico que indica "pronto pra uso"
            // Postgres emite essa msg 2x durante startup
            wait.ForLog("database system is ready to accept connections").
                WithOccurrence(2).
                WithStartupTimeout(5 * time.Second),
        ),
    )
    defer postgres.Terminate(ctx) // cleanup automático
    
    // Container fornece connection string dinâmica (porta aleatória)
    connStr, _ := postgres.ConnectionString(ctx, "sslmode=disable")
    db, _ := sql.Open("postgres", connStr)
    runMigrations(db) // banco limpo para cada teste
    
    // Stack completa: HTTP → Handler → Store → Postgres real
    store := users.NewPostgresStore(db)
    handler := makeUserHandler(store)
    srv := httptest.NewServer(handler)
    
    // Teste de integração real - exatamente como produção
    resp, _ := http.Post(srv.URL+"/users", "application/json",
        strings.NewReader(`{"name":"Alice"}`))
    var created User
    json.NewDecoder(resp.Body).Decode(&created)
    
    // Busca pra validar persistência real
    resp, _ = http.Get(srv.URL + "/users/" + created.ID)
    var fetched User
    json.NewDecoder(resp.Body).Decode(&fetched)
    
    // Sem mocks = sem drift entre teste e produção
    assert.Equal(t, "Alice", fetched.Name)
    assert.NotEmpty(t, fetched.ID)
}

// Teste de contrato com serviço externo que não tem container
func TestPaymentGateway(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test") // pula em CI rápido
    }
    
    // WireMock quando não existe imagem oficial do serviço
    // Ex: Stripe, PayPal, Twilio - APIs proprietárias
    wiremock, _ := wiremock.RunContainer(ctx)
    defer wiremock.Terminate(ctx)
    
    // Define contrato esperado - baseado na doc da API
    client := wiremock.GetClient()
    client.StubFor(wiremock.Post("/charge").
        WithHeader("Authorization", wiremock.EqualTo("Bearer token")).
        WillReturnJSON(map[string]interface{}{
            "id": "ch_123",
            "status": "succeeded",
        }, 200))
    
    // Testa integração com HTTP real, não mocks em memória
    gateway := NewPaymentGateway(wiremock.GetURI())
    result, err := gateway.Charge(ctx, 5000, "token")
    
    // Valida comportamento, não implementação
    assert.NoError(t, err)
    assert.Equal(t, "succeeded", result.Status)
    
    // Pro: testa serialização, headers, timeouts
    // Con: pode divergir da API real (usar Pact pra validar)
}
Vantagens
  • Redução significativa de bugs em produção: Testes com infraestrutura real aumentam confiabilidade
  • Maior confiança em mudanças: Testes de integração validam comportamento real do sistema
  • Validação completa da stack: Todos os aspectos da comunicação são testados
  • Eficiência no uso de recursos: Containers podem ser compartilhados entre múltiplos testes
Trade-offs
  • Tempo e recursos de CI aumentados: Testes de integração levam mais tempo e requerem mais recursos
  • Complexidade de infraestrutura: Configuração de containers e permissões adicionais necessárias
  • Possibilidade de testes intermitentes: Fatores externos podem ocasionalmente afetar execução
  • Curva de aprendizado: Equipe precisa dominar ferramentas de containerização

10. GMUD — CI/CD moderno

GMUD é o processo burocrático tradicional de aprovar mudanças em produção. Go tem características que facilitam a adoção de integração contínua sem depender de processos manuais pesados:

  • Compilador rigoroso — pega erros antes do runtime
  • Testes de integração baratos — como vimos com testcontainers
  • Binário único — deploy é copiar um arquivo
  • Retrocompatibilidade — Go 1 promise garante estabilidade
// CI/CD simples e confiável
func TestMain(m *testing.M) {
    // Testes de integração rodando em CI
    if os.Getenv("CI") == "true" {
        setupTestContainers()
        defer teardownTestContainers()
    }
    
    code := m.Run()
    os.Exit(code)
}

// GitHub Actions
// .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [main]
jobs:
  test-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-go@v4
      - run: go test -race ./...
      - run: go build -o app
      - run: ./deploy.sh  # deploy confiável
Vantagens
  • Frequência de deploy aumentada: Entregas mais frequentes e incrementais
  • Tempo de recuperação reduzido: Rollback simplificado através de troca de binários
  • Redução de incidentes: Mudanças menores apresentam menor risco
  • Feedback mais rápido: Código chega à produção mais rapidamente
Trade-offs
  • Restrições regulatórias: Alguns setores ainda exigem processos formais de aprovação
  • Mudança organizacional necessária: Transição de processos tradicionais para ágeis
  • Infraestrutura sofisticada requerida: Necessidade de ferramentas de observabilidade e deployment avançadas
  • Maior responsabilidade da equipe: Ownership direto sobre mudanças em produção

11. Getters/Setters — Structs abertas

Java popularizou a ideia de encapsular todos os campos com getters/setters. Em Go, structs são abertas por design:

// Abordagem Java-em-Go (não faça isso)
type User struct {
    id   string
    name string
    age  int
}

func (u *User) GetID() string { return u.id }
func (u *User) SetID(id string) { u.id = id }
func (u *User) GetName() string { return u.name }
func (u *User) SetName(name string) { u.name = name }
func (u *User) GetAge() int { return u.age }
func (u *User) SetAge(age int) { u.age = age }

// Go idiomático
type User struct {
    ID   string
    Name string
    Age  int
}

// Validação quando necessária, não por padrão
func NewUser(name string, age int) (*User, error) {
    if age < 0 || age > 150 {
        return nil, errors.New("invalid age")
    }
    return &User{
        ID:   generateID(),
        Name: name,
        Age:  age,
    }, nil
}

// Método apenas quando adiciona comportamento
func (u *User) CanDrink() bool {
    return u.Age >= 18
}
Vantagens
  • Redução substancial de boilerplate: Acesso direto a campos elimina métodos desnecessários
  • Serialização simplificada: Anotações JSON diretamente nos campos
  • Composição natural: Embedding de structs sem necessidade de wrappers
  • Performance otimizada: Acesso direto mais eficiente que chamadas de método
Trade-offs
  • Mudanças de API mais impactantes: Alterações em campos públicos afetam todos os consumidores
  • Validação não automática: Responsabilidade de validação fica com o código consumidor
  • Invariantes requerem disciplina: Manutenção de consistência depende de convenções
  • Ausência de geradores automáticos: Sem anotações para geração automática de código

Estratégias práticas para testes E2E

Quando existe uma imagem Docker oficial

Postgres, Redis, Kafka, NATS, LocalStack já oferecem imagens prontas. Use o módulo dedicado em testcontainers‑go para um DSL enxuto:

redisC, _ := redis.RunContainer(ctx, tc.WithImage("redis:7-alpine"))
url := redisC.GetHostPort("6379/tcp")

Quando não existe imagem — WireMock

Serviços SaaS raramente divulgam binário. Suba um WireMock e reproduza apenas as rotas necessárias:

wm := wiremock.New(ctx)
wm.StubFor(wiremock.Post("/charge").WillReturn("{\"paid\":true}", "application/json", 200))

Stub ultraleve com httptest

Para cenários simples, um servidor local substitui dependências caras:

fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    io.WriteString(w, `{"paid":true}`)
}))

Contratos para evitar drift

Stubs podem descolar da API real. Use Pact (consumer driven) ou Schemathesis (schema driven) em um job noturno que bate no sandbox e acusa quebra.


Trade‑offs honestos

Decisão Benefício Custo / Risco
Wiring explícito (sem DI container) Grafo de dependências visível; debug simples Config útil pode crescer e cansar em sistemas com centenas de serviços
Imutabilidade via cópia Concorrência à prova de race; rollback trivial Structs gigantes (>256 KB) aumentam GC e consumo de RAM
E2E com Testcontainers Praticamente produção em miniatura; confiança alta Teste é mais lento (~1‑3 s), CI precisa de Docker‑in‑Docker ou runner privilegiado
Stubs HTTP CI ultrarrápido, sem custos de API Drift de contrato gera falsos positivos; precisa monitoramento
Evitar camadas "Clean" Menos arquivos, caminhos curtos de leitura Faltam "ganchos" padrão em times grandes; onboard pode precisar de guia

Leia‑se: não existe bala de prata. Cada escolha libera tempo para uma parte do problema e adiciona risco em outra.


Quando esses padrões FAZEM sentido

Seria desonesto não reconhecer cenários onde essas abstrações agregam valor real:

Clean Architecture:

  • Múltiplos times trabalham no mesmo sistema (boundaries claros reduzem conflitos)
  • Compliance regulatório exige rastreabilidade de mudanças (camadas facilitam auditoria)
  • Migração gradual de sistemas legados (abstrações permitem substituição incremental)

DI Containers:

  • Sistema tem 50+ dependências interconectadas
  • Precisa de feature toggles em produção
  • Times querem consistência entre múltiplos microserviços

Testes Unitários:

  • Algoritmos complexos (parsers, calculadoras de impostos, engines de regras)
  • Bibliotecas públicas (garantir contratos de API)
  • Lógica crítica com múltiplos edge cases

A chave é fitness contextual: avalie o custo-benefício para SEU problema específico.


Conclusão

Simplicidade não é fazer menos; é fazer o necessário com maestria. Go encoraja esse mindset: você escreve menos, revê mais rápido, compila em segundos, entrega features que passam no canary. O preço é abrir mão da zona de conforto fornecida por frameworks que fazem tudo. Em troca, o time entende até o byte que cruza o fio.

Insight

"Simplicidade não é fazer menos; é fazer o necessário com maestria."

Dito isso, também não posso deixar de comentar que não concordo totalmente com o modelo mais radical ue o Sibelius propôs na Woovi. Há contextos onde Clean Architecture faz sentido (sistemas com dezenas de times), onde DI containers economizam tempo (aplicações enterprise gigantes), onde testes unitários isolados são valiosos (bibliotecas públicas).

O ponto não é criar uma nova doutrina que substitua a anterior. É questionar dogmas, entender trade-offs e escolher conscientemente. Go nos dá a oportunidade de começar do zero e perguntar: "precisamos mesmo disso?". Na maioria das vezes, a resposta é não.


Leitura Adicional

Recursos Oficiais

Palestras Fundamentais

Ferramentas Mencionadas

Comunidade

  • GopherCon Talks — Arquivo de palestras da principal conferência
  • r/golang — Discussões diárias da comunidade