Pular para o conteúdo

Trilha de Go · parte 56 de 65

Design patterns em Go: da teoria clássica à prática idiomática

Os padrões do Refactoring Guru traduzidos para Go: functional options, factory, singleton, decorator, adapter, strategy, observer e composite, num serviço de pedidos.

intermediárioPublicada em · 45 min de leitura

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

Design patterns em Go: da teoria clássica à prática idiomática

Quando a gangue dos quatro (Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides) publicou o catálogo de 23 padrões de projeto em 1994, o mundo do desenvolvimento estava encantado com hierarquias de herança e taxonomia de classes. Esse catálogo, que o portal Refactoring Guru reorganiza e ilustra em 22 padrões (o Interpreter fica de fora), virou o vocabulário comum de quem escreve software orientado a objetos.

Quando um desenvolvedor acostumado com Java ou C# chega a Go, a primeira reação costuma ser de espanto. Em Go não existem classes, não há herança com extends, não há sobrecarga de métodos e não há polimorfismo por subtipos. Reproduzir à risca a estrutura de classes dos diagramas clássicos gera código burocrático, inchado e anti-idiomático.

Isso não significa que os padrões desapareceram. Os problemas continuam: como criar objetos complexos sem confusão, como estender comportamento sem quebrar contratos, como trocar algoritmos em tempo de execução. O que muda são as ferramentas: composição de structs, interfaces implícitas, funções de primeira classe e concorrência nativa com canais e goroutines.

Paradigma clássico vs filosofia de Go. Cliente OOP → Classe abstrata (depende do tipo concreto); Classe abstrata → Subclasse A (herda estado e métodos)…
figura 1 · Paradigma clássico vs filosofia de Go

À esquerda da figura, a abordagem clássica apoia-se em hierarquias verticais, em que subclasses herdam métodos e atributos de classes abstratas. À direita, Go prefere a composição horizontal: structs concretas guardam os dados, interfaces enxutas de um ou dois métodos definem contratos e closures configuram as instâncias.

As cores seguem o mesmo mapa em toda a aula. Laranja é quem chama o padrão, terracota é o padrão em estudo, verde é o estado saudável, azul é código do próprio serviço, roxo é infraestrutura que atravessa o sistema, rosa é o que dá errado e cinza é o que não controlamos. Na figura, a classe base aparece degradada de propósito: mudar um método dela muda o comportamento de todas as filhas, e o cliente da hierarquia fica preso ao tipo concreto (a seta vermelha). Do lado de Go, o cliente depende só de uma interface, e a struct que a implementa não precisa declarar nada, porque a satisfação é tácita.

Para que servem os padrões

Um padrão de projeto é uma receita para um problema que aparece de novo e de novo. Pense em quem monta estantes: na terceira, você já sabe que fixa o fundo antes de levantar a estrutura. Ninguém inventou isso na hora, é uma solução que deu certo muitas vezes e ganhou nome. Em software, o nome é o que mais vale. Dizer “isso aqui é um Decorator” poupa um parágrafo na revisão de código e diz ao colega onde olhar.

O risco é aplicar a receita sem ter o problema, e em Go isso é comum, porque o catálogo veio de um mundo com classes. Por isso cada padrão desta aula segue o mesmo caminho: a dor que ele resolve, a solução clássica do Refactoring Guru, a solução em Go e o momento em que não vale usá-lo.

Para dar liga, a aula acompanha um serviço de pedidos que vai crescendo. No começo ele tem um construtor com parâmetros demais. Depois ganha vários provedores de pagamento, logs e autenticação, avisos quando um pedido é pago, um banco de dados compartilhado e, no fim, um gargalo de processamento. Cada seção resolve um pedaço desse crescimento.

Todo trecho de código desta aula é um arquivo de um único projeto Go, com o módulo example.com/pedidos. Os tipos conversam entre si: o Order que o checkout cobra é o mesmo que o barramento publica e o mesmo que o handler HTTP recebe. A árvore abaixo mostra onde cada peça mora, e a seção de montagem mostra o main que liga tudo.

pedidos/
├── main.go                    montagem: cria as peças e injeta cada uma
├── server/orders/
│   ├── order.go               Order e as interfaces EventSink e PaymentStrategy
│   ├── service.go             OrderService, Option e NewOrderService
│   ├── checkout.go            Checkout: cobra, grava e publica
│   ├── handler.go             POST /pedidos
│   ├── pipeline.go            gerar, cobrar em paralelo e mesclar
│   ├── itens.go               Node, Produto e Combo
│   └── exemplo_test.go        uso do serviço, compilado pelo go vet
├── storage/store.go           Store e NewStore
├── database/pool.go           Pool, Open e GetPool
├── database/pool_oncevalues.go  a versão com sync.OnceValues
├── middleware/middleware.go   Logging, Auth e Chain
├── adapter/                   AnalyticsAdapter e o cliente do fornecedor
├── tax/tax.go                 Strategy de imposto e InvoiceProcessor
├── payment/payment.go         Pix e Cartao
├── eventbus/bus.go            Bus com Subscribe, Publish e Close
└── trechos/                   exemplos da biblioteca padrão (seção 7)

Mapa da aula

ParteSeçõesPergunta que responde
Mentalidade1O que muda quando o catálogo clássico encontra Go?
Criacionais2Como criar o serviço quando o construtor tem parâmetros demais, vários storages e um banco compartilhado?
Estruturais3Como somar log e autenticação, e como encaixar o SDK de um fornecedor?
Comportamentais4Como trocar a regra de cálculo e avisar quem precisa saber que um pedido foi pago?
Montagem4.3Como o main junta todas as peças sem depender de variáveis globais?
Concorrência5Que padrões a GoF não tinha e Go precisa?
Catálogo completo6 e 7Como cada um dos 22 padrões aparece em Go, e onde a biblioteca padrão já ensina isso?
Prática8 a 11Quais são os erros comuns, os exercícios, o glossário e as fontes?

1. O que muda entre o Refactoring Guru e a filosofia de Go

O catálogo do Refactoring Guru organiza os padrões em três categorias: criacionais, estruturais e comportamentais. Cada um parte de uma intenção clara (o que resolve), com analogias do cotidiano e estruturas orientadas a objetos.

Em Go essa divisão continua útil como modelo mental, mas a implementação muda bastante:

  1. As interfaces são satisfeitas de forma implícita. Um tipo não declara implements. Se ele tem os métodos com as assinaturas esperadas, o compilador aceita. Isso deixa Adapter e Strategy muito leves.
  2. Composição substitui herança. O embute campos e métodos de outro tipo sem criar parentesco na hierarquia de tipos.
  3. Funções são valores de primeira classe. Podem ser atribuídas, passadas e retornadas. Comportamentos que exigiam uma família de classes viram variáveis de tipo função.
  4. Concorrência faz parte do núcleo da linguagem. O modelo clássico trata de chamadas síncronas de método, e Go acrescenta uma categoria de padrões baseada em goroutines e canais.

2. Padrões criacionais em Go

Padrões criacionais cuidam da criação de objetos. A ideia é isolar o resto do sistema de como esses objetos são montados.

2.1 : a resposta de Go ao Builder

Nosso serviço de pedidos começa com um problema simples: o OrderService tem três dependências obrigatórias (a loja de comprovantes, a estratégia de pagamento e o barramento de eventos), além do pool de banco, e ainda aceita imposto, logger e métricas, que são opcionais. Cada peça nova na lista de parâmetros empurra o construtor um pouco mais para longe de ser legível. No Refactoring Guru: Builder o problema é exatamente esse, a construção de objetos com muitos campos opcionais. Em linguagens com sobrecarga, ele leva ao construtor telescópico (versões com 3, 4, 5 argumentos) ou a uma classe auxiliar com métodos encadeados (server.SetTimeout(5).SetMaxConnections(100).Build()).

Go não tem sobrecarga. Um builder mutável também permite estados intermediários inválidos, como chamar Build() antes de preencher um campo obrigatório. A solução da comunidade, popularizada por Rob Pike e Dave Cheney em 2014, é o Functional Options Pattern.

Functional options: visão geral. Chamador → NewOrderService (NewOrderService(db, opts...)); NewOrderService → WithStore(s); NewOrderService → WithPayment(p)…
figura 2 · Functional options: visão geral

A figura de cima mostra o caminho geral. O chamador entra no topo com o pool e uma lista variável de opções. NewOrderService aplica os padrões, executa cada opção sobre o mesmo ponteiro e só então valida. As opções aparecem lado a lado porque nenhuma depende da outra, e a ordem em que chegam é a ordem em que rodam. O losango é a validação final: a seta azul pontilhada e barrada, à direita, mostra que um construtor sem dependências não devolve objeto nenhum, e não um objeto pela metade.

O chamador invoca NewOrderService com o pool e uma quantidade variável de funções de opção. O construtor cria a struct com valores padrão seguros, executa cada closure sobre o ponteiro e só então valida o resultado. Os tipos que o pacote usa ficam em order.go, e o service.go tem a struct, as opções e o construtor:

package orders

import "context"

// Order é o pedido que circula pelo serviço. O valor fica em centavos (int64).
type Order struct {
	ID         string
	TotalCents int64
}

// EventSink é o que o serviço precisa para mandar métricas.
// Quem implementa é um adaptador, que fica em outro pacote.
type EventSink interface {
	Send(payload []byte) error
}

// PaymentStrategy é a forma de cobrança escolhida na configuração.
type PaymentStrategy interface {
	Pay(ctx context.Context, cents int64) (string, error)
}
package orders

import (
	"errors"
	"log/slog"

	"example.com/pedidos/database"
	"example.com/pedidos/eventbus"
	"example.com/pedidos/storage"
	"example.com/pedidos/tax"
)

// OrderService reúne as dependências do checkout.
type OrderService struct {
	db      *database.Pool
	store   storage.Store
	invoice *tax.InvoiceProcessor
	payment PaymentStrategy
	bus     *eventbus.Bus
	metrics EventSink
	logger  *slog.Logger
}

// Option configura o OrderService. Cada opção é uma função que altera o ponteiro.
type Option func(*OrderService)

func WithStore(s storage.Store) Option {
	return func(svc *OrderService) { svc.store = s }
}

func WithInvoice(p *tax.InvoiceProcessor) Option {
	return func(svc *OrderService) { svc.invoice = p }
}

func WithPayment(p PaymentStrategy) Option {
	return func(svc *OrderService) { svc.payment = p }
}

func WithBus(b *eventbus.Bus) Option {
	return func(svc *OrderService) { svc.bus = b }
}

func WithMetrics(m EventSink) Option {
	return func(svc *OrderService) { svc.metrics = m }
}

func WithLogger(l *slog.Logger) Option {
	return func(svc *OrderService) { svc.logger = l }
}

// NewOrderService exige o pool de banco e aplica as opções. Os padrões ficam antes delas.
func NewOrderService(db *database.Pool, opts ...Option) (*OrderService, error) {
	if db == nil {
		return nil, errors.New("o serviço de pedidos precisa de um pool de banco")
	}

	// 1. Padrões seguros: sem imposto e com o logger padrão.
	svc := &OrderService{
		db:      db,
		invoice: tax.NewInvoiceProcessor(tax.Flat(0)),
		logger:  slog.Default(),
	}

	// 2. Opções, na ordem em que foram passadas.
	for _, opt := range opts {
		opt(svc)
	}

	// 3. Validação final, depois de todas as opções.
	switch {
	case svc.store == nil:
		return nil, errors.New("falta o armazenamento de comprovantes (WithStore)")
	case svc.payment == nil:
		return nil, errors.New("falta a estratégia de pagamento (WithPayment)")
	case svc.bus == nil:
		return nil, errors.New("falta o barramento de eventos (WithBus)")
	}
	return svc, nil
}

Quem usa o serviço escreve algo que se documenta sozinho. Este exemplo fica em exemplo_test.go e entra no go vet como um teste de compilação:

package orders_test

import (
	"fmt"

	"example.com/pedidos/database"
	"example.com/pedidos/eventbus"
	"example.com/pedidos/payment"
	"example.com/pedidos/server/orders"
	"example.com/pedidos/storage"
)

func ExampleNewOrderService() {
	pool, err := database.Open("postgres://localhost:5432/pedidos")
	if err != nil {
		fmt.Println(err)
		return
	}
	store, err := storage.NewStore("local", "comprovantes")
	if err != nil {
		fmt.Println(err)
		return
	}
	svc, err := orders.NewOrderService(pool,
		orders.WithStore(store),
		orders.WithPayment(payment.Pix{Chave: "chave-da-loja"}),
		orders.WithBus(eventbus.NewBus()),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	_ = svc
}

Os defaults nunca são esquecidos, novos parâmetros entram sem quebrar a assinatura existente e o objeto já sai do construtor consistente. A figura de baixo mostra a mesma montagem em ordem de tempo, com o zoom no ponteiro: cada opção recebe o mesmo *OrderService e troca um campo. A validação é a última mensagem, e é ela que decide entre devolver o serviço ou o erro.

NewOrderService por dentro. Diagrama com 1 elemento e 0 conexões
figura 3 · NewOrderService por dentro

Vale saber quando não usar. Se a configuração tem poucos campos e todos fazem sentido juntos, uma struct de configuração é mais simples e é o que a própria biblioteca padrão faz em http.Server. O trecho abaixo é uma função que devolve essa configuração, para que o arquivo compile por inteiro:

package trechos

import (
	"net/http"
	"time"
)

// NovoServidor mostra a configuração por struct, como faz a biblioteca padrão.
func NovoServidor() *http.Server {
	return &http.Server{
		Addr:         ":8080",
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 10 * time.Second,
	}
}

Functional options compensam quando há muitos campos opcionais, defaults que precisam de lógica, ou uma API pública que vai crescer sem quebrar quem já a usa.


2.2 Factory Method e funções construtoras (New...)

O serviço agora precisa guardar comprovantes, e em desenvolvimento isso vai para o disco local enquanto em produção vai para o S3. No Refactoring Guru: Factory Method, uma classe criadora delega a instanciação a subclasses por herança. Em Go não existe a classe abstrata Dialog com uma subclasse WindowsDialog só para criar um botão.

Go usa funções construtoras com o nome New ou New<Tipo>. Quando o pacote produz um único tipo principal, a função se chama New, como em ring.New, list.New e errors.New. Quando produz vários, o nome do tipo entra na função: NewUserRepository(db). O pacote tar segue essa segunda regra, com NewReader e NewWriter.

Para alternar entre implementações concretas conforme a configuração, define-se uma interface no pacote que consome e uma função que escolhe a implementação. O arquivo abaixo é storage/store.go por inteiro:

package storage

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path/filepath"
)

// Store é o contrato que o serviço espera para guardar comprovantes.
type Store interface {
	Save(id string, data io.Reader) error
}

type localStorage struct{ basePath string }

func (l *localStorage) Save(id string, data io.Reader) error {
	// Em produção, valide o id antes: ele vira nome de arquivo.
	if err := os.MkdirAll(l.basePath, 0o755); err != nil {
		return err
	}
	f, err := os.Create(filepath.Join(l.basePath, id+".txt"))
	if err != nil {
		return err
	}
	_, copyErr := io.Copy(f, data)
	return errors.Join(copyErr, f.Close())
}

type s3Storage struct{ bucket string }

func (s *s3Storage) Save(id string, data io.Reader) error {
	// O upload real usaria o SDK da AWS. Aqui o trecho só mostra o formato
	// e devolve erro, para não fingir que o comprovante foi guardado.
	return fmt.Errorf("s3 (bucket %q): upload não implementado nesta aula (comprovante %s)", s.bucket, id)
}

// NewStore escolhe a implementação a partir da configuração.
func NewStore(kind string, target string) (Store, error) {
	switch kind {
	case "local":
		return &localStorage{basePath: target}, nil
	case "s3":
		return &s3Storage{bucket: target}, nil
	default:
		return nil, errors.New("mecanismo de armazenamento desconhecido: " + kind)
	}
}

A figura mostra o caminho de decisão. O main lê duas variáveis do ambiente e chama NewStore, que é o único lugar que conhece as duas implementações. O losango é o switch: a seta cheia leva ao disco, a tracejada leva ao S3 e a seta barrada, à direita, leva a um erro que não cria nada. As duas implementações chegam ao mesmo contrato, Store, e é esse contrato que vai para o OrderService. Repare que o s3Storage tem a seta pontilhada: ele existe para mostrar o formato, e o código não o trata como concluído.

Factory: NewStore escolhe a implementação. Variáveis → main (lê as variáveis); main → switch kind (NewStore(kind, target)); switch kind → localStorage (local)…
figura 4 · Factory: NewStore escolhe a implementação

Uma observação honesta: isso é mais próximo do Simple Factory que do Factory Method do livro, porque não há subclasses decidindo o que criar, só um switch. É o que a comunidade Go chama de factory, e funciona bem. Note também que NewStore devolve uma interface, o que parece contrariar o conselho “aceite interfaces, devolva structs” da seção 8. A exceção existe porque aqui a escolha da implementação é justamente o trabalho da função. Quando o construtor só devolve um tipo, devolva a struct concreta.


2.3 Singleton com sync.Once

Vários pontos do serviço usam o mesmo pool de conexões. No Refactoring Guru: Singleton, o objetivo é garantir uma única instância por programa e dar um ponto de acesso global a ela.

Em Go, onde centenas de goroutines podem rodar logo na inicialização, o singleton ingênuo com if instance == nil causa uma race condition. Proteger toda leitura com sync.Mutex resolve a corrida, mas cobra o preço da trava mesmo depois que a instância existe. O sync.Once executa uma função uma única vez e custa quase nada nas chamadas seguintes (veja sync).

Há uma armadilha que a versão mais comum do exemplo esconde. Se a função passada ao Do entrar em pânico, o Once considera que ela já rodou. As chamadas seguintes não tentam de novo e devolvem nil em silêncio, e o erro só aparece mais tarde, como um nil pointer longe da causa. Por isso a versão abaixo guarda o erro em vez de provocar pânico.

A figura mostra o mesmo cenário com três goroutines chamando GetPool ao mesmo tempo. A primeira entra no Do e executa a inicialização. As outras duas, em laranja tracejado com o X, ficam bloqueadas até ela terminar, e não rodam a função de novo. Quando a inicialização dá certo, a seta verde leva o ponteiro para instance, e as três goroutines recebem o mesmo *Pool. Quando dá errado, a seta pontilhada com o X leva o erro para initErr, e essa é a parte que a versão comum esquece: todas as chamadas seguintes recebem o mesmo erro, sem nova tentativa, até o processo reiniciar.

Singleton com sync.Once: uma inicialização, muitas chamadas. G1 → sync.Once.Do (Do(f): entra); G2 → sync.Once.Do (espera a conclusão)…
figura 5 · Singleton com : uma inicialização, muitas chamadas

O código abaixo separa a abertura do pool em Open, que é a peça que o main vai usar, e deixa GetPool como o caminho do Singleton para quem não consegue receber o pool por parâmetro. O arquivo é database/pool.go por inteiro. Na verificação desta aula, a linha do driver foi trocada por um driver falso com o mesmo nome "pgx", para o código rodar sem Postgres. O resto do arquivo é igual ao que roda em produção.

package database

import (
	"database/sql"
	"sync"

	_ "github.com/jackc/pgx/v5/stdlib" // registra o driver "pgx"
)

// Pool embrulha o *sql.DB para que o resto do código não dependa do driver.
type Pool struct{ db *sql.DB }

// Open cria o pool. sql.Open não conecta: a primeira conexão real acontece no primeiro uso.
func Open(dsn string) (*Pool, error) {
	db, err := sql.Open("pgx", dsn)
	if err != nil {
		return nil, err
	}
	return &Pool{db: db}, nil
}

// DB devolve o *sql.DB para quem precisa executar consultas.
func (p *Pool) DB() *sql.DB { return p.db }

var (
	once     sync.Once
	instance *Pool
	initErr  error
)

// GetPool inicializa o pool sob demanda, uma única vez, de forma segura entre goroutines.
func GetPool(dsn string) (*Pool, error) {
	once.Do(func() {
		instance, initErr = Open(dsn)
	})
	return instance, initErr
}

Desde o Go 1.21, a biblioteca padrão tem sync.OnceValue e sync.OnceValues, que embalam esse mesmo raciocínio em uma função só e eliminam as duas variáveis soltas. O arquivo database/pool_oncevalues.go mostra a versão com OnceValues:

package database

import (
	"database/sql"
	"os"
	"sync"
)

// getPool é a versão com sync.OnceValues: a função devolve o pool e o erro, já memorizados.
var getPool = sync.OnceValues(func() (*Pool, error) {
	db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
	if err != nil {
		return nil, err
	}
	return &Pool{db: db}, nil
})

Mesmo assim, o Singleton é suportado, mas variáveis globais são vistas como anti-padrão no Go moderno: escondem dependências e dificultam o teste. O main desta aula usa database.Open e passa o pool para o serviço por opção, então nenhuma variável global participa do caminho principal (veja Injeção de Dependências com Google Wire e injeção de dependência).


3. Padrões estruturais em Go

Padrões estruturais explicam como montar objetos e structs em estruturas maiores sem perder flexibilidade.

3.1 Decorator: a essência dos middlewares

O serviço precisa registrar toda requisição e exigir autenticação, sem encher cada handler de código repetido. O Refactoring Guru: Decorator resolve isso colocando o objeto dentro de um invólucro que contém o comportamento extra. No modelo clássico há uma classe BaseDecorator que implementa a interface do componente e guarda uma referência a ele.

Em Go o mesmo padrão aparece como : uma função que recebe um http.Handler, o envolve com lógica adicional (tempo, auditoria, recuperação de pânico) e devolve outro http.Handler, com a mesma assinatura externa.

Middlewares: decorar e interromper a cadeia. Cliente HTTP → Logging (requisição); Logging → Auth (next.ServeHTTP); Auth → Rate limiter (token ok)…
figura 6 · Middlewares: decorar e interromper a cadeia

A requisição entra pela borda externa, passa pelo log e pela autenticação e chega ao manipulador de negócio. A resposta faz o caminho inverso, e cada camada pode medir ou ajustar o que sai. A seta pontilhada e barrada mostra o que acontece quando a autenticação falha: ela responde 401 sozinha e o restante da cadeia nem chega a ser chamado.

Esse segundo comportamento merece atenção, porque aí o padrão muda de nome. Um middleware que só acrescenta algo e sempre chama o próximo (como o log) é um Decorator. Um que pode decidir parar a cadeia (como a autenticação) se parece mais com a . O Refactoring Guru observa que os dois têm estrutura parecida, e em Go a mesma assinatura serve para ambos. Na prática, chame de middleware e entenda qual dos dois papéis cada um está cumprindo.

A interface da biblioteca padrão é a que um middleware precisa imitar. O trecho abaixo vem de trechos/trechos.go, com os prefixos de pacote, para ficar exato:

// Handler é a interface de net/http, escrita aqui para o exemplo ficar completo.
type Handler interface {
	ServeHTTP(http.ResponseWriter, *http.Request)
}

Um decorador de log que também registra o status exige um detalhe: http.ResponseWriter não deixa ler o status depois de escrito, então embrulhamos o writer para guardá-lo. O arquivo middleware/middleware.go por inteiro reúne os pedaços da cadeia: o tipo Middleware, o statusRecorder, Logging, Auth e Chain.

package middleware

import (
	"log/slog"
	"net/http"
	"time"
)

// Middleware representa o tipo de função decoradora.
type Middleware func(http.Handler) http.Handler

// statusRecorder guarda o status escrito para o log poder lê-lo depois.
type statusRecorder struct {
	http.ResponseWriter
	status int
}

func (r *statusRecorder) WriteHeader(code int) {
	r.status = code
	r.ResponseWriter.WriteHeader(code)
}

// Logging registra método, rota, status e latência de cada requisição.
func Logging(logger *slog.Logger) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			start := time.Now()
			rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}

			next.ServeHTTP(rec, r)

			logger.Info("requisição concluída",
				"método", r.Method,
				"rota", r.URL.Path,
				"status", rec.status,
				"latência", time.Since(start),
			)
		})
	}
}

// Auth interrompe a cadeia com 401 quando o token não confere.
func Auth(token string) Middleware {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			if r.Header.Get("Authorization") != "Bearer "+token {
				http.Error(w, "não autorizado", http.StatusUnauthorized)
				return
			}
			next.ServeHTTP(w, r)
		})
	}
}

// Chain aplica os middlewares de modo que o primeiro da lista fique mais externo.
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
	for i := len(middlewares) - 1; i >= 0; i-- {
		h = middlewares[i](h)
	}
	return h
}

Com Chain(handler, Logging(logger), Auth(token)), o log fica por fora e por isso registra também as respostas 401 da autenticação. Se a ordem fosse invertida, requisições rejeitadas não apareceriam no log.

O handler que a cadeia protege é um http.Handler comum, criado por orders.Handler(svc) em server/orders/handler.go. Ele decodifica o JSON, chama Checkout e responde 201 com o recibo. Os erros de negócio viram 400 ou 500, e a mensagem interna não sai na resposta: ela vai para o log, que é onde ela serve.

package orders

import (
	"encoding/json"
	"net/http"
)

// Handler expõe POST /pedidos. O corpo é JSON: {"id": "p-1", "total_cents": 4990}.
func Handler(svc *OrderService) http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("POST /pedidos", func(w http.ResponseWriter, r *http.Request) {
		var corpo struct {
			ID         string `json:"id"`
			TotalCents int64  `json:"total_cents"`
		}
		if err := json.NewDecoder(r.Body).Decode(&corpo); err != nil {
			http.Error(w, "JSON inválido", http.StatusBadRequest)
			return
		}
		if corpo.TotalCents <= 0 {
			http.Error(w, "total_cents deve ser positivo", http.StatusBadRequest)
			return
		}

		recibo, err := svc.Checkout(r.Context(), Order{ID: corpo.ID, TotalCents: corpo.TotalCents})
		if err != nil {
			svc.logger.Error("checkout falhou", "erro", err)
			http.Error(w, "não foi possível concluir o pedido", http.StatusInternalServerError)
			return
		}

		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusCreated)
		_ = json.NewEncoder(w).Encode(map[string]string{"id": corpo.ID, "recibo": recibo})
	})
	return mux
}

No main, a montagem é uma linha: middleware.Chain(orders.Handler(svc), middleware.Logging(logger), middleware.Auth(token)). Ela deixa claro o que o leitor precisa saber para depurar um 401, que é a ordem da lista.

O mesmo desenho vale para interceptors de gRPC e para decoradores de acesso a banco que capturam métricas do OpenTelemetry (veja Tracing Distribuído e OpenTelemetry).


3.2 Adapter: interfaces implícitas em ação

O serviço precisa mandar eventos para uma ferramenta de métricas de terceiros, cuja biblioteca tem uma assinatura que não combina com a nossa. No Refactoring Guru: Adapter, o adaptador traduz entre dois objetos de interfaces incompatíveis.

Em Go o Adapter fica leve por duas regras de design. A primeira: a interface é definida por quem a consome, não por quem a implementa. A segunda: interfaces de um método só têm um poder de abstração enorme (io.Reader, io.Writer, fmt.Stringer).

Nosso código espera um gravador simples. O contrato EventSink já apareceu em order.go, na seção 2.1, e é ele que o OrderService usa para mandar métricas. A figura mostra a tradução: o Checkout chama Send numa interface que é nossa, o AnalyticsAdapter responde a essa chamada e fala a língua do fornecedor, e o fornecedor nem sabe que existe um EventSink.

Adapter: o fornecedor não fala a nossa interface. OrderService → AnalyticsAdapter (Send(payload)); AnalyticsAdapter → EventSink (satisfaz)…
figura 7 · Adapter: o fornecedor não fala a nossa interface

Na figura, o caminho vai da esquerda para a direita. O pedido sai do serviço como Send(payload), entra no adaptador e vira uma chamada a TrackCustomEvent, com o nome do evento fixo em pedido_pago. A linha pontilhada que sobe até o EventSink é a satisfação tácita: o adaptador não declara nada, ele só tem o método com a assinatura certa. A seta de ida e volta é a chamada e a resposta. Quando o fornecedor devolve false, a seta barrada mostra o caminho de falha, em que o adaptador converte a recusa em erro. Por isso o Checkout registra um aviso e segue: uma métrica recusada não cancela um pedido que já foi cobrado.

O fornecedor entrega isto. O arquivo adapter/fornecedor.go é o código dele, que não controlamos:

package adapter

// ThirdPartyAnalyticsClient simula o SDK de um fornecedor. Esse código não é nosso:
// a assinatura de TrackCustomEvent é a que o fornecedor escolheu.
type ThirdPartyAnalyticsClient struct{}

func (c *ThirdPartyAnalyticsClient) TrackCustomEvent(name string, rawData []byte) (bool, error) {
	// implementação do fornecedor: aceita qualquer evento com nome
	return name != "", nil
}

O adaptador é uma struct pequena que faz o de-para. O arquivo adapter/analytics.go traz a struct, o Send e a linha de verificação:

package adapter

import (
	"errors"

	"example.com/pedidos/server/orders"
)

// AnalyticsAdapter traduz a interface EventSink para a assinatura do fornecedor.
type AnalyticsAdapter struct {
	client    *ThirdPartyAnalyticsClient
	eventName string
}

func NewAnalyticsAdapter(client *ThirdPartyAnalyticsClient, eventName string) *AnalyticsAdapter {
	return &AnalyticsAdapter{client: client, eventName: eventName}
}

// Send satisfaz orders.EventSink sem declarar vínculo explícito com a interface.
func (a *AnalyticsAdapter) Send(payload []byte) error {
	ok, err := a.client.TrackCustomEvent(a.eventName, payload)
	if err != nil {
		return err
	}
	if !ok {
		return errors.New("fornecedor recusou o evento " + a.eventName)
	}
	return nil
}

// Garantia em tempo de compilação de que o adaptador cumpre o contrato.
var _ orders.EventSink = (*AnalyticsAdapter)(nil)

A linha var _ orders.EventSink = (*AnalyticsAdapter)(nil) é um truque comum: se alguém mudar a assinatura de Send, o erro aparece na compilação do pacote e não só no ponto de uso. O restante do código fica isolado das mudanças na biblioteca externa.

O exemplo mais famoso de Adapter na biblioteca padrão está na seção 7: http.HandlerFunc, que transforma uma função comum em um http.Handler.


4. Padrões comportamentais em Go

Padrões comportamentais tratam de como os objetos dividem responsabilidades e se comunicam.

4.1 Strategy: funções como estratégias

O serviço calcula impostos de formas diferentes conforme o cliente, e aceita formas de cobrança diferentes conforme a configuração. No Refactoring Guru: Strategy, define-se uma família de algoritmos, cada um em uma classe, e os objetos ficam intercambiáveis. O exemplo clássico é o cálculo de rotas (a pé, de carro, de transporte público).

Em Java ou C++ isso pede uma interface RouteStrategy, uma classe por algoritmo e uma classe de contexto que guarda a estratégia atual. Em Go há duas formas, conforme a complexidade. No checkout do serviço aparecem as duas: o imposto é uma função, e a cobrança é uma interface. A figura mostra as duas trocas no mesmo fluxo.

Strategy: o checkout troca a regra sem mudar o fluxo. tax.Flat(600) → tax.Strategy (injeta); PAGAMENTO → PaymentStrategy (injeta)…
figura 8 · Strategy: o checkout troca a regra sem mudar o fluxo

Leia da esquerda para a direita. As duas escolhas à esquerda chegam por injeção: a alíquota vira uma função e o meio de pagamento vira uma implementação de PaymentStrategy. O Checkout, no centro, faz sempre a mesma sequência, calcula o imposto, cobra e segue. As setas de ida e volta são as duas chamadas que ele faz às estratégias. A seta barrada à direita é a falha que a cobrança pode ter: com payment.Cartao e zero parcelas, a estratégia recusa, o erro sobe e o pedido não é gravado.

Abordagem 1: funções de primeira classe

Quando o algoritmo é uma transformação pura, sem estado, a assinatura da função já é a estratégia. Para dinheiro, use inteiros em centavos: float64 acumula erros de arredondamento. As alíquotas abaixo são fictícias, só para o exemplo (impostos reais costumam ter faixas e regras que mudam):

package tax

// Strategy calcula o imposto, em centavos, sobre um valor em centavos.
type Strategy func(cents int64) int64

// Flat devolve uma estratégia de alíquota fixa, em pontos-base (600 = 6%).
func Flat(basisPoints int64) Strategy {
	return func(cents int64) int64 { return cents * basisPoints / 10_000 }
}

// InvoiceProcessor usa a estratégia recebida na criação.
type InvoiceProcessor struct {
	strategy Strategy
}

func NewInvoiceProcessor(strategy Strategy) *InvoiceProcessor {
	return &InvoiceProcessor{strategy: strategy}
}

func (p *InvoiceProcessor) Compute(orderTotalCents int64) int64 {
	return p.strategy(orderTotalCents)
}

No serviço, WithInvoice(tax.NewInvoiceProcessor(tax.Flat(600))) aplica 6% sobre o total, e NewOrderService começa com tax.Flat(0), que não cobra imposto. Trocar a regra é trocar o argumento, e o Checkout não muda uma linha.

Abordagem 2: interfaces enxutas

Se a estratégia precisa de conexão de rede, cache ou chave de API, uma interface de método único cabe melhor, porque o estado mora no tipo que a implementa. A interface PaymentStrategy fica em order.go, junto ao serviço que a usa, e as implementações estão em payment/payment.go:

package payment

import (
	"context"
	"errors"
	"fmt"
)

// Pix cobra à vista. O recibo aqui é só uma string de exemplo.
type Pix struct{ Chave string }

func (p Pix) Pay(ctx context.Context, cents int64) (string, error) {
	if err := ctx.Err(); err != nil {
		return "", err
	}
	return fmt.Sprintf("pix:%s:%d", p.Chave, cents), nil
}

// Cartao cobra em parcelas. Com zero parcelas, a cobrança é recusada.
type Cartao struct{ Parcelas int }

func (c Cartao) Pay(ctx context.Context, cents int64) (string, error) {
	if err := ctx.Err(); err != nil {
		return "", err
	}
	if c.Parcelas < 1 {
		return "", errors.New("cartão: o número de parcelas deve ser pelo menos 1")
	}
	return fmt.Sprintf("cartao:%dx:%d", c.Parcelas, cents), nil
}

O Checkout chama as duas estratégias no meio do fluxo. O arquivo inteiro está em checkout.go, e a seção 4.2 explica a última linha, a publicação do evento:

package orders

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"example.com/pedidos/eventbus"
)

// Checkout cobra o pedido, guarda o comprovante, grava no banco e publica "pedido.pago".
// Devolve o recibo do meio de pagamento.
func (s *OrderService) Checkout(ctx context.Context, o Order) (string, error) {
	if o.TotalCents <= 0 {
		return "", fmt.Errorf("pedido %s: o valor deve ser positivo", o.ID)
	}
	cobrado := o.TotalCents + s.invoice.Compute(o.TotalCents)

	recibo, err := s.payment.Pay(ctx, cobrado)
	if err != nil {
		return "", fmt.Errorf("cobrança do pedido %s: %w", o.ID, err)
	}
	if err := s.store.Save(o.ID, strings.NewReader(recibo)); err != nil {
		return "", fmt.Errorf("comprovante do pedido %s: %w", o.ID, err)
	}
	_, err = s.db.DB().ExecContext(ctx,
		`INSERT INTO pedidos (id, total_cents, recibo) VALUES ($1, $2, $3)`,
		o.ID, cobrado, recibo)
	if err != nil {
		return "", fmt.Errorf("gravar pedido %s: %w", o.ID, err)
	}

	if n := s.bus.Publish(eventbus.Event{Topic: "pedido.pago", Data: o}); n > 0 {
		s.logger.Warn("evento descartado por assinante lento", "pedido", o.ID, "assinantes", n)
	}
	s.enviarMetrica(o.ID, cobrado)
	return recibo, nil
}

// enviarMetrica não derruba o checkout: a métrica perdida é um problema de painel,
// o pedido perdido é um problema de caixa.
func (s *OrderService) enviarMetrica(id string, cobrado int64) {
	if s.metrics == nil {
		return
	}
	payload, err := json.Marshal(struct {
		Evento   string `json:"evento"`
		Pedido   string `json:"pedido"`
		Centavos int64  `json:"centavos"`
	}{"pedido.pago", id, cobrado})
	if err != nil {
		s.logger.Error("métrica não serializada", "erro", err)
		return
	}
	if err := s.metrics.Send(payload); err != nil {
		s.logger.Warn("métrica não enviada", "pedido", id, "erro", err)
	}
}

Qualquer tipo com Pay entra no fluxo de checkout sem mudar o orquestrador. A biblioteca padrão usa as duas formas: slices.SortFunc recebe uma função como estratégia de comparação, e sort.Interface recebe um tipo com três métodos.


4.2 Observer: canais e eventos concorrentes

Quando um pedido é pago, o estoque, o e-mail e o financeiro precisam saber. O Refactoring Guru: Observer descreve um mecanismo de assinatura: o sujeito mantém uma lista de ouvintes e chama update(data) em cada um de forma síncrona.

Em Go esse modelo síncrono tem uma armadilha séria. Se um ouvinte trava ou demora, ele bloqueia o produtor e todos os outros. Por isso o Observer em Go costuma usar canais e goroutines, o que desacopla produtor e ouvinte no tempo.

Observer com canais: o ouvinte lento perde eventos. Serviço de pedidos → Publish (Event{pedido.pago}); Publish → estoque (entrega); Publish → email (entrega)…
figura 9 · Observer com canais: o ouvinte lento perde eventos

O produtor publica no barramento, que entrega o evento no canal de cada assinante. O assinante rápido consome na hora. O lento deixa o buffer encher e, em vez de travar todos, passa a ter eventos descartados (a seta bloqueada na figura). Essa escolha é de projeto: aqui prefere-se perder um evento a parar o sistema. Se perder não é aceitável, o buffer deve ser maior, a publicação deve esperar, ou o evento deve ir para uma fila durável.

package eventbus

import "sync"

type Event struct {
	Topic string
	Data  any
}

// Bus gerencia assinaturas com segurança entre goroutines.
type Bus struct {
	mu     sync.RWMutex
	subs   map[string]map[chan Event]struct{}
	closed bool
}

func NewBus() *Bus {
	return &Bus{subs: make(map[string]map[chan Event]struct{})}
}

// Subscribe devolve o canal de eventos e uma função para cancelar a assinatura.
func (b *Bus) Subscribe(topic string) (<-chan Event, func()) {
	b.mu.Lock()
	defer b.mu.Unlock()

	ch := make(chan Event, 64) // o buffer evita travar o produtor por pouco tempo
	if b.closed {
		close(ch)
		return ch, func() {}
	}
	if b.subs[topic] == nil {
		b.subs[topic] = make(map[chan Event]struct{})
	}
	b.subs[topic][ch] = struct{}{}

	unsubscribe := func() {
		b.mu.Lock()
		defer b.mu.Unlock()
		if _, ok := b.subs[topic][ch]; ok {
			delete(b.subs[topic], ch)
			close(ch)
		}
	}
	return ch, unsubscribe
}

// Publish entrega o evento sem bloquear. Devolve quantos assinantes ficaram sem ele.
func (b *Bus) Publish(e Event) (dropped int) {
	b.mu.RLock()
	defer b.mu.RUnlock()

	for ch := range b.subs[e.Topic] {
		select {
		case ch <- e:
		default:
			dropped++ // buffer cheio: o ouvinte lento perde o evento
		}
	}
	return dropped
}

// Close encerra todas as assinaturas e libera as goroutines que leem os canais.
func (b *Bus) Close() {
	b.mu.Lock()
	defer b.mu.Unlock()

	b.closed = true
	for _, set := range b.subs {
		for ch := range set {
			close(ch)
		}
	}
	b.subs = make(map[string]map[chan Event]struct{})
}

Cada assinante roda em sua goroutine e termina quando o canal fecha. A função assinar, do main desta aula, faz isso uma vez por consumidor e devolve a função que cancela a assinatura:

// assinar cria um assinante do pedido.pago e devolve a função que o cancela.
func assinar(bus *eventbus.Bus, nome string, logger *slog.Logger) func() {
	eventos, cancelar := bus.Subscribe("pedido.pago")
	go func() {
		for e := range eventos { // o range termina quando o canal fecha
			pedido, ok := e.Data.(orders.Order)
			if !ok {
				logger.Error("evento de tipo inesperado", "consumidor", nome)
				continue
			}
			logger.Info("pedido recebido", "consumidor", nome, "pedido", pedido.ID, "centavos", pedido.TotalCents)
		}
	}()
	return cancelar
}

No checkout, a publicação é a linha if n := s.bus.Publish(eventbus.Event{Topic: "pedido.pago", Data: o}); n > 0 {, em checkout.go. O Publish não espera ninguém. O número que ele devolve diz quantos assinantes perderam o evento por estarem com o buffer cheio, e o checkout registra isso em log, sem tratar como erro do pedido.

Sem o Unsubscribe e o Close, canais e goroutines ficariam vivos para sempre, o clássico vazamento de goroutine (veja Goroutine, Channels e Select). Quando a notificação é simples e interna, uma lista de callbacks []func(Event) também é Observer e dispensa canais, e é o que a biblioteca padrão usa em http.Server.RegisterOnShutdown e context.AfterFunc.


4.3 Juntando tudo: o main como montagem

O main é o único arquivo que conhece todas as peças pelo nome. Ele cria cada uma, uma vez, e passa para quem precisa, por parâmetro ou por opção. Nenhuma peça procura outra numa variável global, e isso aparece na prática: para trocar o S3 pelo disco num teste, basta mudar a chamada do NewStore, sem caçar um uso escondido de singleton. A figura mostra a montagem em camadas: as peças criadas no topo, o serviço no centro, e a entrada HTTP e os assinantes nas pontas.

Montagem do serviço no main. database.Open → orders.NewOrderService; storage.NewStore → orders.NewOrderService; payment.Pix ou Cartao → orders.NewOrderService…
figura 10 · Montagem do serviço no main

Leia de cima para baixo. As cinco peças do grupo superior entram no OrderService por opção, e ele não sabe se o pool veio do Postgres ou de um teste. O laranja marca o que vem da configuração, como o meio de pagamento e a alíquota. O roxo é o Chain, que atravessa toda requisição. A requisição desce pela entrada, passa pelo Chain e chega ao handler, que chama o Checkout. A seta verde do centro é a publicação do pedido.pago, que se espalha para os três assinantes com o mesmo evento.

O arquivo main.go tem a montagem completa. Ela tem duas saídas: sem a flag -lote, sobe o servidor HTTP e espera o sinal de encerramento; com -lote N, processa N pedidos de teste pelo pipeline da seção 5 e sai:

package main

import (
	"context"
	"errors"
	"flag"
	"fmt"
	"log/slog"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"example.com/pedidos/adapter"
	"example.com/pedidos/database"
	"example.com/pedidos/eventbus"
	"example.com/pedidos/middleware"
	"example.com/pedidos/payment"
	"example.com/pedidos/server/orders"
	"example.com/pedidos/storage"
	"example.com/pedidos/tax"
)

func main() {
	if err := run(); err != nil {
		slog.Error("serviço encerrado com erro", "erro", err)
		os.Exit(1)
	}
}

func run() error {
	lote := flag.Int("lote", 0, "processa N pedidos de teste pelo pipeline e sai")
	flag.Parse()

	// Ctrl+C e SIGTERM cancelam este contexto, que chega a todos os estágios.
	ctx, parar := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer parar()

	logger := slog.New(slog.NewTextHandler(os.Stdout, nil))

	// 1. Infraestrutura. Cada peça nasce aqui e segue por parâmetro. Nada é global.
	pool, err := database.Open(envOr("DATABASE_URL", "postgres://localhost:5432/pedidos"))
	if err != nil {
		return err
	}
	pagamento, err := escolherPagamento(os.Getenv("PAGAMENTO"))
	if err != nil {
		return err
	}
	store, err := storage.NewStore(envOr("STORAGE", "local"), envOr("STORAGE_DIR", "comprovantes"))
	if err != nil {
		return err
	}
	bus := eventbus.NewBus()
	defer bus.Close()
	metricas := adapter.NewAnalyticsAdapter(&adapter.ThirdPartyAnalyticsClient{}, "pedido_pago")

	// 2. O serviço recebe tudo pelas opções.
	svc, err := orders.NewOrderService(pool,
		orders.WithStore(store),
		orders.WithInvoice(tax.NewInvoiceProcessor(tax.Flat(600))),
		orders.WithPayment(pagamento),
		orders.WithBus(bus),
		orders.WithMetrics(metricas),
		orders.WithLogger(logger),
	)
	if err != nil {
		return err
	}

	// 3. Assinantes do pedido.pago. Cada um tem sua goroutine e sua função de cancelamento.
	for _, nome := range []string{"estoque", "email", "financeiro"} {
		cancelar := assinar(bus, nome, logger)
		defer cancelar()
	}

	if *lote > 0 {
		return rodarLote(ctx, svc, *lote, logger)
	}

	// 4. Servidor HTTP. Logging fica por fora, então registra também os 401.
	token := os.Getenv("TOKEN")
	if token == "" {
		return errors.New("defina TOKEN para o middleware Auth")
	}
	handler := middleware.Chain(orders.Handler(svc), middleware.Logging(logger), middleware.Auth(token))
	srv := &http.Server{
		Addr:              envOr("ADDR", ":8080"),
		Handler:           handler,
		ReadHeaderTimeout: 5 * time.Second,
	}

	erros := make(chan error, 1)
	go func() { erros <- srv.ListenAndServe() }()
	logger.Info("servidor no ar", "endereço", srv.Addr)

	select {
	case err := <-erros:
		return err
	case <-ctx.Done():
	}

	// 5. Desligamento gracioso: para de aceitar conexões e espera as que estão em curso.
	ctxDesligar, cancelarDesligar := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancelarDesligar()
	return srv.Shutdown(ctxDesligar)
}

// assinar cria um assinante do pedido.pago e devolve a função que o cancela.
func assinar(bus *eventbus.Bus, nome string, logger *slog.Logger) func() {
	eventos, cancelar := bus.Subscribe("pedido.pago")
	go func() {
		for e := range eventos { // o range termina quando o canal fecha
			pedido, ok := e.Data.(orders.Order)
			if !ok {
				logger.Error("evento de tipo inesperado", "consumidor", nome)
				continue
			}
			logger.Info("pedido recebido", "consumidor", nome, "pedido", pedido.ID, "centavos", pedido.TotalCents)
		}
	}()
	return cancelar
}

// rodarLote alimenta o pipeline com pedidos de teste e conta os resultados.
func rodarLote(ctx context.Context, svc *orders.OrderService, n int, logger *slog.Logger) error {
	pedidos := make([]orders.Order, n)
	for i := range pedidos {
		pedidos[i] = orders.Order{ID: fmt.Sprintf("p-%03d", i+1), TotalCents: int64(1000 + 150*i)}
	}
	ok, falhas := 0, 0
	for r := range svc.ProcessarLote(ctx, pedidos, 4) {
		if r.Err != nil {
			falhas++
			logger.Warn("pedido não processado", "pedido", r.Pedido.ID, "erro", r.Err)
			continue
		}
		ok++
	}
	logger.Info("lote concluído", "ok", ok, "falhas", falhas)
	return nil
}

// escolherPagamento é o ponto de troca da Strategy: o resto do serviço não sabe qual foi escolhido.
func escolherPagamento(nome string) (orders.PaymentStrategy, error) {
	switch nome {
	case "", "pix":
		return payment.Pix{Chave: "chave-da-loja"}, nil
	case "cartao":
		return payment.Cartao{Parcelas: 3}, nil
	default:
		return nil, fmt.Errorf("pagamento desconhecido: %q", nome)
	}
}

func envOr(chave, padrao string) string {
	if v := os.Getenv(chave); v != "" {
		return v
	}
	return padrao
}

Os defer rodam em ordem inversa. Por isso os cancelamentos dos assinantes saem antes do bus.Close, e o barramento fecha por último. Se faltar alguma peça, NewOrderService devolve erro e o main termina com código 1 antes de abrir a porta. É a validação final do construtor em ação, e é o que impede o serviço de existir pela metade.

O driver real do Postgres exige um banco com a tabela pedidos, e a verificação desta aula rodou com o driver falso. Com ele, os comandos abaixo mostram o comportamento completo:

TOKEN=segredo ADDR=127.0.0.1:8081 go run .
curl -X POST localhost:8081/pedidos -H "Authorization: Bearer segredo" -d '{"id":"h-2","total_cents":4990}'
go run . -lote 20

5. A quarta família: padrões concorrentes de Go

O catálogo de 1994 foi pensado para programas de um só fluxo de execução, e a engenharia em Go lida o tempo todo com vários. Por isso a comunidade consolidou padrões que o livro não tem. Voltando ao serviço de pedidos: quando ele passa a processar milhares de pedidos por minuto, o gargalo pede estes:

  1. Pipeline (Generator, Fan-Out, Fan-In). Divide um processamento pesado em estágios ligados por canais. Quando um estágio vira gargalo, vários workers leem do mesmo canal (Fan-Out) e um multiplexador reúne os resultados (Fan-In). Detalhado em Pipeline Pattern e Fan-Out, Fan-In.
  2. Worker Pool e Rate Limiting. Limita o número de goroutines ativas sobre uma fila de tarefas, o que protege as conexões do banco e a memória. Detalhado em Worker Pools e Rate Limiting.
  3. Cancelamento em cascata com Context. Propaga timeout, deadline e cancelamento por toda a árvore de chamadas. Detalhado em Context.
  4. Grupo de tarefas com errgroup. Dispara várias goroutines e devolve o primeiro erro, cancelando as demais. Vive em golang.org/x/sync/errgroup.
  5. Laço for com select. A estrutura que quase todo worker tem: ouvir o trabalho e o sinal de parada no mesmo laço (veja Select).

O lote de pedidos é o caso concreto. Cada pedido exige uma cobrança, que demora porque chama um gateway, e o banco aguenta um número limitado de conexões ao mesmo tempo. O pipeline resolve isso em três estágios: o gerador produz os pedidos, um conjunto fixo de workers cobra em paralelo e o mesclador junta os resultados num canal só. O arquivo pipeline.go tem os três estágios e a função ProcessarLote, que os liga.

Pipeline: gerar, cobrar em paralelo e mesclar. gerar → pedidos (Order); pedidos → Worker 1 (round-robin); pedidos → Worker 2 (round-robin)…
figura 11 · Pipeline: gerar, cobrar em paralelo e mesclar

O caminho da figura vai da esquerda para a direita. O gerador enche o canal pedidos, que tem quatro posições de buffer. Os três workers do meio leem desse canal, e a seta marcada como round-robin é uma aproximação: no código, quem pega o próximo pedido é o primeiro worker livre. Os resultados voltam pelo mesclador e saem pelo canal resultados, que o consumidor lê com range. O contexto, em roxo, é o único controle de parada: quando ele é cancelado, as setas pontilhadas chegam ao gerador e ao mesclador, e cada estágio sai do seu laço. Não há cor de falha no pipeline, porque a falha de um pedido vira um Resultado com erro, e o pipeline continua.

package orders

import (
	"context"
	"sync"
)

// Resultado é o que sai do pipeline para cada pedido: o recibo ou o erro.
type Resultado struct {
	Pedido Order
	Recibo string
	Err    error
}

// gerar é o primeiro estágio (generator): produz os pedidos e para se o contexto for cancelado.
func gerar(ctx context.Context, pedidos []Order) <-chan Order {
	saida := make(chan Order)
	go func() {
		defer close(saida)
		for _, p := range pedidos {
			select {
			case saida <- p:
			case <-ctx.Done():
				return
			}
		}
	}()
	return saida
}

// cobrar é o fan-out: n workers leem da mesma entrada, e cada um cobra o que recebe.
func (s *OrderService) cobrar(ctx context.Context, entrada <-chan Order, n int) []<-chan Resultado {
	saidas := make([]<-chan Resultado, n)
	for i := range n {
		saidas[i] = s.trabalhador(ctx, entrada)
	}
	return saidas
}

// trabalhador é um worker do pool: lê um pedido, cobra e devolve o resultado.
// Tanto a leitura quanto a escrita olham o ctx.Done(), então o cancelamento nunca o deixa preso.
func (s *OrderService) trabalhador(ctx context.Context, entrada <-chan Order) <-chan Resultado {
	saida := make(chan Resultado)
	go func() {
		defer close(saida)
		for {
			select {
			case <-ctx.Done():
				return
			case p, ok := <-entrada:
				if !ok {
					return
				}
				recibo, err := s.Checkout(ctx, p)
				select {
				case saida <- Resultado{Pedido: p, Recibo: recibo, Err: err}:
				case <-ctx.Done():
					return
				}
			}
		}
	}()
	return saida
}

// mesclar é o fan-in: junta várias saídas em um único canal.
func mesclar(ctx context.Context, canais []<-chan Resultado) <-chan Resultado {
	saida := make(chan Resultado)
	var wg sync.WaitGroup

	repassar := func(c <-chan Resultado) {
		defer wg.Done()
		for r := range c {
			select {
			case saida <- r:
			case <-ctx.Done():
				return
			}
		}
	}

	wg.Add(len(canais))
	for _, c := range canais {
		go repassar(c)
	}
	go func() {
		wg.Wait()
		close(saida)
	}()
	return saida
}

// ProcessarLote liga os estágios: gerar, cobrar com n workers e mesclar.
// O canal devolvido fecha quando todos os pedidos saíram ou quando o contexto é cancelado.
func (s *OrderService) ProcessarLote(ctx context.Context, pedidos []Order, workers int) <-chan Resultado {
	if workers < 1 {
		workers = 1 // zero workers deixaria o gerador preso sem ninguém para ler
	}
	entrada := gerar(ctx, pedidos)
	return mesclar(ctx, s.cobrar(ctx, entrada, workers))
}

Dois detalhes do código importam. Todo envio e todo recebimento do pipeline aparece num select com ctx.Done(), porque sem isso um estágio ficaria parado esperando um leitor que já foi embora. E ProcessarLote garante pelo menos um worker, porque zero workers deixaria o gerador preso, sem ninguém para ler o que ele produz.

O trabalhador é o worker pool em si: um conjunto fixo de goroutines que leem a mesma fila, e o limite de workers é o que protege o banco. Ele lê um pedido, cobra e envia o resultado, e tanto a leitura quanto o envio passam pelo select. Esse for { select { ... } } é o item 5 da lista, a estrutura que todo worker tem.

A figura compara os dois caminhos de cancelamento. Em cima, o worker escuta o contexto: quando o cancel() chega, o select escolhe Done, a goroutine retorna e o defer close(saida) fecha o canal. Embaixo, o consumidor sai do laço com break sem cancelar nada, e o worker fica parado no envio, porque o buffer está cheio e ninguém mais lê. Essa goroutine nunca termina, e é o vazamento clássico de pipeline em Go. Ele costuma passar despercebido em testes pequenos, porque o canal raramente enche.

Cancelamento: o caminho saudável e o vazamento. ctx → Worker (Done()); Worker → saida (envio com select); Worker → Encerrado (return ao receber Done)…
figura 12 · Cancelamento: o caminho saudável e o vazamento

O erro cabe numa função. Ela compila, mas quem a chama fica preso se ninguém mais ler o canal, e é esse o padrão que a figura de baixo mostra:

package trechos

import "example.com/pedidos/server/orders"

// EnviarSemCancelamento é o erro comum: o envio fica bloqueado para sempre se ninguém mais ler.
func EnviarSemCancelamento(saida chan<- orders.Resultado, r orders.Resultado) {
	saida <- r
}

A versão correta é o select que aparece nos dois lados do trabalhador. O teste TestProcessarLoteEntregaTodosEPermiteCancelar, em service_test.go, cobre o caminho saudável: cancela depois do primeiro resultado e confere que o canal fecha em até dois segundos. Se alguém remover um select do pipeline, esse teste deixa de passar.


6. O catálogo completo do Refactoring Guru em Go

A tabela abaixo cobre os 22 padrões. Os mais importantes têm seção própria acima, e os demais ficam em uma linha, porque em Go muitos deles encolhem para uma função ou uma interface.

PadrãoAbordagem clássica OOP (Java/C++)Equivalente em Go
Factory MethodSubclasses sobrescrevendo um método abstratoFunções New... (seção 2.2)
Abstract FactoryFamílias de fábricas com hierarquias paralelasInterface com os métodos de criação e uma implementação por família. Raro, e a seção 8 explica por quê
BuilderClasse separada com métodos encadeados e Build()Functional options (seção 2.1)
PrototypeMétodo clone() em cada classeCopiar a struct por valor (b := a), com cuidado para slices e maps, que continuam compartilhados. Um método Clone() quando a cópia profunda importa
SingletonConstrutor privado e campo estáticosync.Once ou sync.OnceValue, ou melhor, injeção no main() (seção 2.3)
AdapterClasse que estende o alvo ou implementa a interfaceStruct pequena que satisfaz a interface, ou http.HandlerFunc (seções 3.2 e 7)
BridgeDuas hierarquias ligadas por composiçãoUma struct que guarda uma interface. É o jeito normal de escrever Go
CompositeÁrvore com classe base abstrata e nós compostosUm nó que guarda []Interface da própria interface (veja o exemplo abaixo)
DecoratorClasses envelope da mesma interface baseMiddlewares, e io.Reader encadeados (seções 3.1 e 7)
FacadeClasse que esconde um subsistemaUm pacote com poucas funções exportadas que esconde os internos
FlyweightObjetos compartilhados para economizar memóriaInterning de valores repetidos, o pacote unique (Go 1.23) e sync.Pool para reaproveitar objetos
ProxySubstituto com a mesma interfaceStruct que implementa a mesma interface e delega ao real (cache, lazy, controle de acesso)
Chain of ResponsibilityCadeia de nós com next e classe abstrataCadeia de handlers que podem parar o fluxo, como a autenticação (seção 3.1)
CommandObjeto que encapsula uma açãoUma func() ou uma struct com Execute(). Filas de tarefas e worker pools recebem funções
IteratorObjeto com hasNext() e next()range sobre slices, maps e canais, e iter.Seq desde o Go 1.23 (seção 7)
MediatorObjeto central que coordena os demaisUm barramento de eventos ou uma goroutine que recebe mensagens por canal
MementoObjeto que guarda um snapshot do estadoCopiar a struct por valor, ou serializar o estado
ObserverSujeito com lista de ouvintes e update() síncronoCanais e select não bloqueante, ou callbacks (seção 4.2)
StateUma classe por estadoUm type Estado int com switch, ou uma função que devolve a próxima função de estado
StrategyFamília de classes com a mesma interfaceFunções de primeira classe ou interfaces de um método (seção 4.1)
Template MethodClasse base com um esqueleto e ganchos sobrescritosUma função que recebe funções ou uma interface com os passos variáveis. Embedding não funciona aqui (veja o Erro 2)
VisitorMétodo accept(visitor) em cada classeUma função de visita passada a quem percorre a estrutura, como fs.WalkDir e ast.Inspect

O merece um exemplo que mostre a recursão de verdade. O clássico usa File e Dir; aqui os mesmos papéis são Produto e Combo, porque um kit é uma árvore de itens de pedido. Node é a interface comum às folhas e às composições, com um método só: Total. O Combo soma os filhos chamando Total em cada um, sem perguntar se é produto ou outro combo:

Composite: combo feito de produtos e de combos. Pedido → Combo Kit família (Total()); Combo Kit família → Combo Bebidas; Combo Kit família → Pizza G…
figura 13 · Composite: combo feito de produtos e de combos

O caminho da árvore vai de cima para baixo. O pedido chama Total() uma vez, na raiz, e a chamada desce pela árvore em laranja. Cada combo repete a mesma chamada nos próprios filhos, e essa repetição é a recursão. As setas verdes de volta mostram o valor subindo. O combo Kit família soma três filhos (dois combos e uma pizza), e o Bebidas soma dois produtos. Os selos de Total conferem a conta: 1690 mais 4990 mais 1190 dá 7870 centavos. As folhas são os produtos, que não têm filhos e respondem com o próprio valor, e é aí que a recursão para.

package orders

// Node é o contrato comum de quem compõe um pedido: um produto ou um combo.
type Node interface {
	Total() int64
}

// Produto é a folha: tem valor próprio e não tem filhos.
type Produto struct {
	Nome  string
	Cents int64
}

func (p Produto) Total() int64 { return p.Cents }

// Combo é a composição: soma os filhos, que podem ser produtos ou outros combos.
type Combo struct {
	Nome  string
	Itens []Node
}

func (c Combo) Total() (soma int64) {
	for _, item := range c.Itens {
		soma += item.Total() // não importa se o filho é produto ou combo
	}
	return soma
}

Um Combo dentro de outro Combo funciona sem código extra, e quem chama Total() não precisa saber se está diante de um produto ou de uma árvore. O mesmo desenho está em io.MultiWriter, que é um io.Writer formado por vários io.Writer. Embutir interfaces, como em io.ReadWriter, junta contratos e é outra coisa.


7. A biblioteca padrão como professora

A melhor fonte de padrões idiomáticos de Go é a própria biblioteca padrão, e o código dela já passou por anos de revisão. Quatro exemplos mostram o catálogo em uso. Os trechos abaixo ficam no pacote trechos, usam os tipos do serviço quando faz sentido e compilam junto com o resto do módulo.

Decorator com io.Reader. Cada camada recebe um io.Reader e devolve outro, e elas se empilham. É o Decorator mais clássico de Go, mais antigo que a moda dos middlewares:

// LerComCamadas empilha três leitores. Quem consome não sabe que há três camadas.
func LerComCamadas(origem io.Reader, copia io.Writer) (string, error) {
	limitado := io.LimitReader(origem, 1<<20)  // limita a 1 MiB
	espelhado := io.TeeReader(limitado, copia) // copia o que passa para `copia`
	bufferizado := bufio.NewReader(espelhado)  // acrescenta um buffer
	conteudo, err := io.ReadAll(bufferizado)
	return string(conteudo), err
}

Adapter com http.HandlerFunc. Uma função comum não é um http.Handler, porque não tem o método ServeHTTP. O tipo HandlerFunc é uma função com um método que a chama, o que a transforma em handler. A interface Handler aparece antes, para o exemplo ficar completo:

// Handler é a interface de net/http, escrita aqui para o exemplo ficar completo.
type Handler interface {
	ServeHTTP(http.ResponseWriter, *http.Request)
}

// HandlerFunc é o truque de net/http: uma função ganha um método e vira handler.
type HandlerFunc func(http.ResponseWriter, *http.Request)

func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) { f(w, r) }

var _ Handler = HandlerFunc(nil)

Esse truque de dar um método a um tipo função é o jeito mais curto de adaptar uma função a uma interface, e serve para qualquer contrato de um método.

Strategy com slices.SortFunc. A comparação é a estratégia, passada como função. A função OrdenarPorValor usa a comparação por TotalCents, do maior para o menor, e é o ponto de partida do Exercício 3:

// OrdenarPorValor usa a função de comparação como estratégia de ordenação.
func OrdenarPorValor(pedidos []orders.Order) {
	slices.SortFunc(pedidos, func(a, b orders.Order) int { return cmp.Compare(b.TotalCents, a.TotalCents) })
}

Iterator com iter.Seq. Desde o Go 1.23, uma função que recebe um yield pode ser usada direto no range, e cobre o que o hasNext()/next() fazia nas outras linguagens. Parar o laço com break faz o yield devolver false, e a função deve respeitar isso. O teste TestGrandesParaCedo, em trechos_test.go, confirma que o break interrompe a sequência antes do último pedido:

// Grandes filtra os pedidos acima de um valor sem montar uma lista intermediária.
func Grandes(pedidos []orders.Order, min int64) iter.Seq[orders.Order] {
	return func(yield func(orders.Order) bool) {
		for _, p := range pedidos {
			if p.TotalCents >= min && !yield(p) {
				return
			}
		}
	}
}
func TestGrandesParaCedo(t *testing.T) {
	pedidos := []orders.Order{{ID: "a", TotalCents: 100}, {ID: "b", TotalCents: 900}, {ID: "c", TotalCents: 500}}
	n := 0
	for p := range Grandes(pedidos, 300) {
		n++
		if p.ID == "b" {
			break
		}
	}
	if n != 1 {
		t.Fatalf("laço visitou %d pedidos, esperado 1 (break antes do c)", n)
	}
}

Há ainda um registro de fábricas escondido em database/sql: o driver se registra no init() com sql.Register, e o import em branco (_ "github.com/jackc/pgx/v5/stdlib") basta para disponibilizar o nome "pgx" em sql.Open. É uma fábrica sem switch no código de quem usa. Outros exemplos: fs.WalkDir como Visitor e sync.Pool como pool de objetos (veja sync).


8. Erros comuns, armadilhas e quando não usar um padrão

Ao aplicar padrões em Go, quem vem de outros ecossistemas costuma cair em excesso de engenharia.

Erro 1: criar interfaces antes de precisar delas

No mundo corporativo tradicional é comum o par UserService.java (interface) e UserServiceImpl.java (classe). Em Go isso é um erro. Com uma implementação só, devolva a struct concreta e deixe a interface para o pacote que consome, quando houver necessidade real de um mock para testes ou de várias implementações. O conselho conhecido é “aceite interfaces, devolva structs” (veja Mocks e Dublês de Teste). A interface vive no consumidor porque é ele quem sabe o pouco que precisa do outro lado.

Erro 2: simular herança com struct embedding

O embedding promove campos e métodos do tipo embutido para o tipo externo, mas o externo não é um subtipo do interno. Se o método do tipo interno chama outro método dele mesmo, não chega à versão “sobrescrita” do externo. Não existe tabela virtual entre structs:

package trechos

type Base struct{}

func (Base) Name() string    { return "base" }
func (b Base) Hello() string { return "olá, " + b.Name() }

type Child struct{ Base }

func (Child) Name() string { return "child" }

// Child{}.Hello() devolve "olá, base" e não "olá, child".

Quem tenta fazer um Template Method desse jeito leva essa surpresa. Em Go, passe os passos variáveis como funções ou como uma interface, e use composição só para reaproveitar código.

Erro 3: fábricas de fábricas

Não crie AbstractWidgetFactoryProvider. A maioria dos componentes em Go precisa de um New() simples que recebe as dependências e devolve (*Tipo, error). É por isso que o Abstract Factory aparece pouco: uma interface com os métodos de criação, passada ao consumidor, já cobre os casos reais.

Erro 4: usar padrão sem ter o problema

Antes de aplicar um padrão, pergunte qual dor ele resolve neste código. Os Go Proverbs resumem a postura: um pouco de cópia é melhor que uma pequena dependência, e claro é melhor que esperto. Se uma função simples resolve, ela vence a hierarquia, o registro de plugins e a fábrica configurável. Um padrão se justifica quando o código real já mostra a repetição ou a rigidez que ele remove, e não porque o catálogo o lista.


9. Exercícios práticos

Exercício 1: timeout de checkout

Acrescente ao OrderService a opção WithCheckoutTimeout(d time.Duration), com padrão de 5 segundos. O Checkout deve usar context.WithTimeout, para que a cobrança não passe desse limite. Na validação final de NewOrderService, rejeite um d menor ou igual a zero. Dica: o padrão entra antes das opções, como o imposto e o logger já entram, e a validação do valor só pode acontecer depois que todas as opções foram aplicadas. Teste com uma PaymentStrategy falsa que demora mais que o limite.

Exercício 2: middleware de métricas de execução

Escreva um middleware para http.Handler que meça o tempo do handler interno. Acima de 200 milissegundos, emita um alerta com log/slog contendo método, URI e tempo em milissegundos. Dica: se quiser incluir o status, reaproveite a ideia do statusRecorder da seção 3.1. Depois, escreva um teste com httptest que confirme a ordem de uma cadeia com dois middlewares.

Exercício 3: strategy para ordenação de relatórios

Defina um tipo função SortStrategy que ordene uma lista de orders.Order. Crie duas implementações: uma pelo maior valor (TotalCents, em centavos, como int64) e outra pelo ID, em ordem alfabética. Escreva a função que recebe a lista e a estratégia e gera o relatório. Dica: slices.SortFunc e cmp.Compare resolvem a comparação, e OrdenarPorValor, da seção 7, é o ponto de partida. O Order não tem data; se você acrescentar um campo Criado time.Time, a ordenação pela data mais recente vira um terceiro caso.

Exercício 4: composite e iterator

Reuse o Node da seção 6 e acrescente ao Combo um método Produtos() iter.Seq[Produto] que percorra toda a árvore, inclusive os combos dentro de combos. Use-o para listar os produtos do kit da figura, do mais caro para o mais barato, e confira que a soma bate com os 7870 centavos da seção 6.

Exercício 5: observer sem vazamento

Escreva um teste para o Bus que publique 100 eventos para um assinante lento e confirme quantos foram descartados. Depois chame Close e verifique que a goroutine do assinante terminou (dica: runtime.NumGoroutine antes e depois, ou um sync.WaitGroup).


Pergunta de entrevistaQuando não vale usar um padrão de projeto?

Quando ele só adiciona indireção sem resolver uma dor real. Em Go, vários padrões clássicos têm forma mais simples: strategy costuma ser uma função, decorator é um tipo que embrulha uma interface, e factory pode ser uma função comum. Um padrão se justifica quando há variação real de comportamento. Caso contrário, código simples é mais fácil de ler e de mudar.

10. Glossário

  • Functional Options: padrão criacional de Go em que closures passadas como argumentos variádicos configuram uma struct de forma extensível e segura.
  • Tipagem estrutural (duck typing estático): a compatibilidade entre um tipo e uma interface é decidida pelos métodos que o tipo tem, sem declaração explícita.
  • Struct embedding: incluir um tipo dentro de outro para promover seus campos e métodos, sem criar herança.
  • Middleware: decorador funcional que intercepta o ciclo de uma requisição para acrescentar comportamento transversal, como segurança, telemetria e rate limiting.
  • sync.Once: primitivo da biblioteca padrão que executa um bloco exatamente uma vez, com proteção contra corridas. sync.OnceValue e sync.OnceValues (Go 1.21) devolvem também o resultado.
  • Composite: padrão em que folhas e composições compartilham uma interface, e a composição aplica a operação a seus filhos.
  • Chain of Responsibility: cadeia de handlers em que cada um decide tratar o pedido, passá-lo adiante ou interromper o fluxo.
  • iter.Seq: tipo de função (Go 1.23) que representa uma sequência que pode ser percorrida com range.

11. Fontes consultadas

Consultadas em 29 de outubro de 2026.

  • Refactoring Guru: catálogo de design patterns, que lista 22 padrões (5 criacionais, 7 estruturais e 10 comportamentais).
  • Dave Cheney: Functional options for friendly APIs.
  • Rob Pike: Self-referential functions and the design of options.
  • The Go Blog e a documentação oficial: Effective Go.
  • Go Proverbs.
  • Todo bloco de código Go desta nota é um trecho literal de um arquivo do módulo example.com/pedidos, com go 1.23 no go.mod e a toolchain go1.27.2. Em 29 de outubro de 2026 o módulo passou por gofmt -l, go vet ./..., go test -race -count=1 ./... e pelo go run nos dois modos: go run . -lote 20, e o servidor com curl (respostas 401, 201 e 400). Na verificação, o driver pgx foi trocado por um driver falso com o mesmo nome (internal/fakepgx), então nenhuma consulta SQL rodou num Postgres real. Os trechos NovoServidor e EnviarSemCancelamento foram compilados, mas não executados.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?