Artigo 47 — Design Patterns em Go: Repository, Factory e Strategy
Curso: Dominando Go em 1 Ano Prof. Ricardo Matos Módulo 8 — Arquitetura e Padrões
Patterns em Go: idiomáticos, não traduzidos
Design Patterns do livro Gang of Four foram concebidos para linguagens orientadas a objetos com herança. Aplicar esses padrões literalmente em Go — tentando recriar classes abstratas com interfaces e herança com embedding — produz código artificialmente complexo.
A abordagem correta é diferente: entender o problema que cada padrão resolve e implementá-lo da forma mais natural em Go. Muitos padrões clássicos se tornam simplesmente "usar interfaces" ou "passar funções como parâmetros". Outros se traduzem naturalmente. Este artigo foca nos padrões mais relevantes para o dia a dia de um desenvolvedor Go.
Repository Pattern
O padrão Repository abstrai o acesso a dados por trás de uma interface, permitindo que a lógica de negócio não conheça detalhes de persistência.
// Definição da interface — no pacote de domínio ou casos de uso
type RepositorioProduto interface {
Criar(ctx context.Context, p *Produto) error
BuscarPorID(ctx context.Context, id int64) (*Produto, error)
BuscarPorSlug(ctx context.Context, slug string) (*Produto, error)
Listar(ctx context.Context, filtro FiltroProduto) ([]*Produto, error)
Atualizar(ctx context.Context, p *Produto) error
Deletar(ctx context.Context, id int64) error
Contar(ctx context.Context, filtro FiltroProduto) (int64, error)
}
// Implementação PostgreSQL
type repoProdutoPostgres struct {
db *pgxpool.Pool
}
func NovoRepoProdutoPostgres(db *pgxpool.Pool) RepositorioProduto {
return &repoProdutoPostgres{db: db}
}
func (r *repoProdutoPostgres) BuscarPorID(ctx context.Context, id int64) (*Produto, error) {
query := `
SELECT id, nome, slug, descricao, preco, estoque, ativo, criado_em
FROM produtos
WHERE id = $1 AND deletado_em IS NULL
`
rows, err := r.db.Query(ctx, query, id)
if err != nil {
return nil, fmt.Errorf("buscar produto: %w", err)
}
produto, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Produto])
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrProdutoNaoEncontrado
}
return produto, err
}
func (r *repoProdutoPostgres) Listar(ctx context.Context, f FiltroProduto) ([]*Produto, error) {
query := `
SELECT id, nome, slug, descricao, preco, estoque, ativo, criado_em
FROM produtos
WHERE deletado_em IS NULL
AND ($1::bigint IS NULL OR categoria_id = $1)
AND ($2::boolean IS NULL OR ativo = $2)
AND ($3::text IS NULL OR nome ILIKE '%' || $3 || '%')
ORDER BY nome
LIMIT $4 OFFSET $5
`
rows, err := r.db.Query(ctx, query,
f.CategoriaID, f.Ativo, f.Busca,
f.Limite, f.Offset,
)
if err != nil {
return nil, fmt.Errorf("listar produtos: %w", err)
}
return pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Produto])
}
// Implementação em memória — para testes
type repoProdutoMemoria struct {
mu sync.RWMutex
dados map[int64]*Produto
proximo int64
}
func NovoRepoProdutoMemoria() RepositorioProduto {
return &repoProdutoMemoria{
dados: make(map[int64]*Produto),
proximo: 1,
}
}
func (r *repoProdutoMemoria) Criar(_ context.Context, p *Produto) error {
r.mu.Lock()
defer r.mu.Unlock()
p.ID = r.proximo
copia := *p
r.dados[r.proximo] = &copia
r.proximo++
return nil
}
func (r *repoProdutoMemoria) BuscarPorID(_ context.Context, id int64) (*Produto, error) {
r.mu.RLock()
defer r.mu.RUnlock()
p, existe := r.dados[id]
if !existe {
return nil, ErrProdutoNaoEncontrado
}
copia := *p
return &copia, nil
}
// ... demais métodos
O repositório em memória é usado em testes de integração — sem banco de dados, com velocidade máxima e comportamento idêntico ao repositório real.
Factory Pattern
Factory encapsula a criação de objetos complexos, centralizando a lógica de construção e escondendo detalhes de implementação.
// Factory simples: função que retorna uma interface
type Notificador interface {
Enviar(ctx context.Context, dest, assunto, corpo string) error
Nome() string
}
// Implementações concretas
type notificadorEmail struct {
host string
porta int
usuario string
senha string
}
func (n *notificadorEmail) Nome() string { return "email" }
func (n *notificadorEmail) Enviar(ctx context.Context, dest, assunto, corpo string) error {
fmt.Printf("[EMAIL] Para: %s | Assunto: %s\n", dest, assunto)
return nil
}
type notificadorSMS struct {
apiKey string
origem string
}
func (n *notificadorSMS) Nome() string { return "sms" }
func (n *notificadorSMS) Enviar(ctx context.Context, dest, _, corpo string) error {
fmt.Printf("[SMS] Para: %s | Mensagem: %s\n", dest, corpo)
return nil
}
type notificadorPush struct {
serverKey string
}
func (n *notificadorPush) Nome() string { return "push" }
func (n *notificadorPush) Enviar(ctx context.Context, dest, assunto, corpo string) error {
fmt.Printf("[PUSH] Token: %s | %s: %s\n", dest, assunto, corpo)
return nil
}
// Factory: cria o notificador correto baseado na configuração
type ConfigNotificador struct {
Tipo string
Host string
Porta int
APIKey string
Usuario string
Senha string
}
func CriarNotificador(cfg ConfigNotificador) (Notificador, error) {
switch strings.ToLower(cfg.Tipo) {
case "email":
if cfg.Host == "" {
return nil, errors.New("host SMTP obrigatório para email")
}
return ¬ificadorEmail{
host: cfg.Host,
porta: cfg.Porta,
usuario: cfg.Usuario,
senha: cfg.Senha,
}, nil
case "sms":
if cfg.APIKey == "" {
return nil, errors.New("API key obrigatória para SMS")
}
return ¬ificadorSMS{apiKey: cfg.APIKey}, nil
case "push":
if cfg.APIKey == "" {
return nil, errors.New("server key obrigatória para push")
}
return ¬ificadorPush{serverKey: cfg.APIKey}, nil
default:
return nil, fmt.Errorf("tipo de notificador desconhecido: %s", cfg.Tipo)
}
}
// Factory com registro dinâmico — extensível sem modificar o factory
type FactoryNotificador struct {
construtores map[string]func(ConfigNotificador) (Notificador, error)
}
func NovoFactoryNotificador() *FactoryNotificador {
f := &FactoryNotificador{
construtores: make(map[string]func(ConfigNotificador) (Notificador, error)),
}
// Registra tipos padrão
f.Registrar("email", func(cfg ConfigNotificador) (Notificador, error) {
return ¬ificadorEmail{host: cfg.Host, porta: cfg.Porta}, nil
})
f.Registrar("sms", func(cfg ConfigNotificador) (Notificador, error) {
return ¬ificadorSMS{apiKey: cfg.APIKey}, nil
})
return f
}
func (f *FactoryNotificador) Registrar(tipo string, construtor func(ConfigNotificador) (Notificador, error)) {
f.construtores[strings.ToLower(tipo)] = construtor
}
func (f *FactoryNotificador) Criar(cfg ConfigNotificador) (Notificador, error) {
construtor, existe := f.construtores[strings.ToLower(cfg.Tipo)]
if !existe {
return nil, fmt.Errorf("tipo '%s' não registrado", cfg.Tipo)
}
return construtor(cfg)
}
Strategy Pattern
Strategy define uma família de algoritmos intercambiáveis. Em Go, isso é simplesmente passar funções ou interfaces como parâmetros.
// Estratégia como tipo de função — a forma mais Go
type EstrategiaOrdenacao func(itens []Produto) []Produto
func ordenarPorPrecoAsc(itens []Produto) []Produto {
copia := make([]Produto, len(itens))
copy(copia, itens)
sort.Slice(copia, func(i, j int) bool {
return copia[i].Preco < copia[j].Preco
})
return copia
}
func ordenarPorPrecoDesc(itens []Produto) []Produto {
copia := make([]Produto, len(itens))
copy(copia, itens)
sort.Slice(copia, func(i, j int) bool {
return copia[i].Preco > copia[j].Preco
})
return copia
}
func ordenarPorNome(itens []Produto) []Produto {
copia := make([]Produto, len(itens))
copy(copia, itens)
sort.Slice(copia, func(i, j int) bool {
return copia[i].Nome < copia[j].Nome
})
return copia
}
func ordenarPorRelevancia(query string) EstrategiaOrdenacao {
return func(itens []Produto) []Produto {
// ordenação por relevância com base na query
copia := make([]Produto, len(itens))
copy(copia, itens)
sort.Slice(copia, func(i, j int) bool {
ri := strings.Contains(strings.ToLower(copia[i].Nome), strings.ToLower(query))
rj := strings.Contains(strings.ToLower(copia[j].Nome), strings.ToLower(query))
return ri && !rj
})
return copia
}
}
// Serviço que usa a estratégia
type ServicoCatalogo struct {
repo RepositorioProduto
}
func (s *ServicoCatalogo) Listar(
ctx context.Context,
filtro FiltroProduto,
ordenar EstrategiaOrdenacao,
) ([]*Produto, error) {
produtos, err := s.repo.Listar(ctx, filtro)
if err != nil {
return nil, err
}
if ordenar == nil {
return produtos, nil
}
// Converte []*Produto para []Produto, ordena, converte de volta
itens := make([]Produto, len(produtos))
for i, p := range produtos {
itens[i] = *p
}
ordenados := ordenar(itens)
resultado := make([]*Produto, len(ordenados))
for i := range ordenados {
copia := ordenados[i]
resultado[i] = &copia
}
return resultado, nil
}
// Strategy como interface — quando o estado é necessário
type EstrategiaDesconto interface {
Calcular(preco float64, cliente *Cliente) float64
Descricao() string
}
type descontoPercentual struct {
percentual float64
}
func NovoDescontoPercentual(p float64) EstrategiaDesconto {
return &descontoPercentual{percentual: p}
}
func (d *descontoPercentual) Calcular(preco float64, _ *Cliente) float64 {
return preco * (1 - d.percentual/100)
}
func (d *descontoPercentual) Descricao() string {
return fmt.Sprintf("%.0f%% de desconto", d.percentual)
}
type descontoVIP struct {
percentualBase float64
percentualBonus float64
minimoCompras int
}
func (d *descontoVIP) Calcular(preco float64, cliente *Cliente) float64 {
if cliente.TotalCompras() >= d.minimoCompras {
return preco * (1 - (d.percentualBase+d.percentualBonus)/100)
}
return preco * (1 - d.percentualBase/100)
}
func (d *descontoVIP) Descricao() string {
return fmt.Sprintf("Desconto VIP: %.0f%% base + %.0f%% bonus",
d.percentualBase, d.percentualBonus)
}
type semDesconto struct{}
func (s *semDesconto) Calcular(preco float64, _ *Cliente) float64 { return preco }
func (s *semDesconto) Descricao() string { return "sem desconto" }
// Context que usa a estratégia
type CalculadorPreco struct {
estrategia EstrategiaDesconto
}
func NovoCalculadorPreco(e EstrategiaDesconto) *CalculadorPreco {
if e == nil {
e = &semDesconto{}
}
return &CalculadorPreco{estrategia: e}
}
func (c *CalculadorPreco) AlterarEstrategia(e EstrategiaDesconto) {
c.estrategia = e
}
func (c *CalculadorPreco) Calcular(preco float64, cliente *Cliente) (float64, string) {
final := c.estrategia.Calcular(preco, cliente)
return final, c.estrategia.Descricao()
}
Observer Pattern
Observer notifica múltiplos componentes quando um evento ocorre, sem que o emissor conheça os receptores.
// Observer via canais — idiomático em Go
type EventoPedido struct {
Tipo string
PedidoID int64
ClienteID int64
Total float64
OcorridoEm time.Time
}
type ObservadorPedido interface {
Processar(ctx context.Context, evento EventoPedido) error
Nome() string
}
type BusEventosPedido struct {
mu sync.RWMutex
observadores []ObservadorPedido
}
func NovoBusEventosPedido() *BusEventosPedido {
return &BusEventosPedido{}
}
func (b *BusEventosPedido) Registrar(obs ObservadorPedido) {
b.mu.Lock()
defer b.mu.Unlock()
b.observadores = append(b.observadores, obs)
}
func (b *BusEventosPedido) Publicar(ctx context.Context, evento EventoPedido) {
b.mu.RLock()
obs := make([]ObservadorPedido, len(b.observadores))
copy(obs, b.observadores)
b.mu.RUnlock()
for _, o := range obs {
o := o
go func() {
if err := o.Processar(ctx, evento); err != nil {
slog.Error("observador falhou",
"observador", o.Nome(),
"evento", evento.Tipo,
"err", err,
)
}
}()
}
}
// Observadores concretos
type observadorEmail struct{ mailer Mailer }
func (o *observadorEmail) Nome() string { return "email" }
func (o *observadorEmail) Processar(ctx context.Context, e EventoPedido) error {
if e.Tipo != "pedido.confirmado" {
return nil
}
return o.mailer.EnviarConfirmacaoPedido(ctx, e.ClienteID, e.PedidoID, e.Total)
}
type observadorEstoque struct{ repo RepositorioProduto }
func (o *observadorEstoque) Nome() string { return "estoque" }
func (o *observadorEstoque) Processar(ctx context.Context, e EventoPedido) error {
if e.Tipo != "pedido.cancelado" {
return nil
}
return o.repo.ReporEstoque(ctx, e.PedidoID)
}
type observadorAnalytics struct{ cliente ClienteAnalytics }
func (o *observadorAnalytics) Nome() string { return "analytics" }
func (o *observadorAnalytics) Processar(ctx context.Context, e EventoPedido) error {
return o.cliente.RegistrarEvento(ctx, e.Tipo, map[string]any{
"pedido_id": e.PedidoID,
"total": e.Total,
})
}
Decorator Pattern
Decorator adiciona comportamento a um objeto sem modificar sua estrutura. Em Go, é implementado criando uma struct que envolve outra struct implementando a mesma interface.
// Decorator de cache sobre um repositório
type repoProdutoComCache struct {
repo RepositorioProduto
cache Cache
ttl time.Duration
}
func NovoRepoProdutoComCache(repo RepositorioProduto, cache Cache, ttl time.Duration) RepositorioProduto {
return &repoProdutoComCache{repo: repo, cache: cache, ttl: ttl}
}
func (r *repoProdutoComCache) BuscarPorID(ctx context.Context, id int64) (*Produto, error) {
chave := fmt.Sprintf("produto:%d", id)
// Tenta o cache primeiro
if val, existe := r.cache.Get(ctx, chave); existe {
var p Produto
if err := json.Unmarshal([]byte(val), &p); err == nil {
return &p, nil
}
}
// Cache miss — busca no repositório
produto, err := r.repo.BuscarPorID(ctx, id)
if err != nil {
return nil, err
}
// Armazena no cache
if dados, err := json.Marshal(produto); err == nil {
r.cache.Set(ctx, chave, string(dados), r.ttl)
}
return produto, nil
}
func (r *repoProdutoComCache) Atualizar(ctx context.Context, p *Produto) error {
if err := r.repo.Atualizar(ctx, p); err != nil {
return err
}
// Invalida o cache após atualização
r.cache.Delete(ctx, fmt.Sprintf("produto:%d", p.ID))
return nil
}
// Delega os demais métodos diretamente
func (r *repoProdutoComCache) Criar(ctx context.Context, p *Produto) error {
return r.repo.Criar(ctx, p)
}
func (r *repoProdutoComCache) BuscarPorSlug(ctx context.Context, slug string) (*Produto, error) {
return r.repo.BuscarPorSlug(ctx, slug)
}
func (r *repoProdutoComCache) Listar(ctx context.Context, f FiltroProduto) ([]*Produto, error) {
return r.repo.Listar(ctx, f)
}
func (r *repoProdutoComCache) Deletar(ctx context.Context, id int64) error {
if err := r.repo.Deletar(ctx, id); err != nil {
return err
}
r.cache.Delete(ctx, fmt.Sprintf("produto:%d", id))
return nil
}
func (r *repoProdutoComCache) Contar(ctx context.Context, f FiltroProduto) (int64, error) {
return r.repo.Contar(ctx, f)
}
// Decorator de logging
type repoProdutoComLog struct {
repo RepositorioProduto
logger *slog.Logger
}
func NovoRepoProdutoComLog(repo RepositorioProduto, logger *slog.Logger) RepositorioProduto {
return &repoProdutoComLog{repo: repo, logger: logger}
}
func (r *repoProdutoComLog) BuscarPorID(ctx context.Context, id int64) (*Produto, error) {
inicio := time.Now()
produto, err := r.repo.BuscarPorID(ctx, id)
r.logger.Debug("repo.BuscarPorID",
"id", id,
"duração_ms", time.Since(inicio).Milliseconds(),
"encontrado", produto != nil,
"erro", err,
)
return produto, err
}
// Composição de decorators no main
func configurarRepoProduto(
db *pgxpool.Pool,
cache Cache,
logger *slog.Logger,
) RepositorioProduto {
var repo RepositorioProduto
repo = NovoRepoProdutoPostgres(db) // base
repo = NovoRepoProdutoComCache(repo, cache, time.Hour) // + cache
repo = NovoRepoProdutoComLog(repo, logger) // + logging
return repo
}
Builder Pattern
Builder constrói objetos complexos passo a passo:
type ConsultaSQL struct {
tabela string
condicoes []string
args []any
ordenacao []string
limite int
offset int
joins []string
}
type BuilderConsulta struct {
c ConsultaSQL
}
func NovaConsulta(tabela string) *BuilderConsulta {
return &BuilderConsulta{c: ConsultaSQL{tabela: tabela}}
}
func (b *BuilderConsulta) Onde(condicao string, args ...any) *BuilderConsulta {
b.c.condicoes = append(b.c.condicoes, condicao)
b.c.args = append(b.c.args, args...)
return b
}
func (b *BuilderConsulta) OrderBy(campo string, desc bool) *BuilderConsulta {
dir := "ASC"
if desc {
dir = "DESC"
}
b.c.ordenacao = append(b.c.ordenacao, campo+" "+dir)
return b
}
func (b *BuilderConsulta) Limite(n int) *BuilderConsulta {
b.c.limite = n
return b
}
func (b *BuilderConsulta) Offset(n int) *BuilderConsulta {
b.c.offset = n
return b
}
func (b *BuilderConsulta) Join(join string) *BuilderConsulta {
b.c.joins = append(b.c.joins, join)
return b
}
func (b *BuilderConsulta) Construir() (string, []any) {
sql := "SELECT * FROM " + b.c.tabela
for _, join := range b.c.joins {
sql += " " + join
}
if len(b.c.condicoes) > 0 {
sql += " WHERE " + strings.Join(b.c.condicoes, " AND ")
}
if len(b.c.ordenacao) > 0 {
sql += " ORDER BY " + strings.Join(b.c.ordenacao, ", ")
}
if b.c.limite > 0 {
sql += fmt.Sprintf(" LIMIT %d", b.c.limite)
}
if b.c.offset > 0 {
sql += fmt.Sprintf(" OFFSET %d", b.c.offset)
}
return sql, b.c.args
}
// Uso
sql, args := NovaConsulta("produtos").
Join("INNER JOIN categorias c ON c.id = produtos.categoria_id").
Onde("produtos.ativo = $1", true).
Onde("c.slug = $2", "eletronicos").
OrderBy("produtos.preco", false).
Limite(20).
Offset(40).
Construir()
Resumo do que foi coberto
Este artigo apresentou Design Patterns idiomáticos em Go: Repository com implementações intercambiáveis para PostgreSQL e memória, Factory com registro dinâmico de tipos, Strategy com funções e interfaces para algoritmos intercambiáveis, Observer com goroutines para notificação assíncrona, Decorator para adicionar comportamento — cache e logging — sem modificar implementações existentes, e Builder para construção passo a passo de objetos complexos. O próximo artigo constrói uma API RESTful completa do zero ao deploy.
Referências e leituras complementares
-
Go Patterns — Catálogo de padrões idiomáticos em Go. https://github.com/tmrts/go-patterns
-
Refactoring Guru — Go — Implementações de todos os padrões GoF em Go. https://refactoring.guru/design-patterns/go
-
Functional Options — Dave Cheney — Artigo seminal sobre o padrão de opções funcionais. https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
-
Go Blog: Error handling and Go — Como o padrão de erros em Go se relaciona com os padrões clássicos. https://go.dev/blog/error-handling-and-go
-
Repository Pattern in Go — Artigo prático sobre implementação do padrão Repository. https://threedots.tech/post/repository-pattern-in-go/
-
Practical Go — Real world advice — Dave Cheney sobre padrões práticos em Go. https://dave.cheney.net/practical-go
Próximo artigo: Artigo 48 — APIs RESTful completas: projeto do zero ao deploy**
you asked
Sim