Design Patterns em Go: Repository, Factory e Strategy

Design Patterns em Go: Repository, Factory e Strategy

Design Patterns em Go medidos: Repository, Factory com registro que não perde campos, Strategy com ordenação estável, Observer com contexto sem cancelamento, Decorator por embedding da interface e um Builder de SQL que numera os placeholders sozinho.
Golang

• • 20 min de leitura

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 casos de uso — sem banco de dados e com velocidade máxima. O comportamento, porém, só é parecido com o do real: ele não tem restrições como UNIQUE, transações nem as particularidades do SQL, e os testes do repositório PostgreSQL continuam precisando de um banco de verdade.

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 &notificadorEmail{
            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 &notificadorSMS{apiKey: cfg.APIKey}, nil

    case "push":
        if cfg.APIKey == "" {
            return nil, errors.New("server key obrigatória para push")
        }
        return &notificadorPush{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 os tipos padrão reaproveitando CriarNotificador: as mesmas
    // validações, e nenhum campo da configuração esquecido no caminho
    for _, tipo := range []string{"email", "sms", "push"} {
        f.Registrar(tipo, CriarNotificador)
    }

    return f
}

// Registrar deve ser chamado na inicialização: o mapa não é protegido por
// mutex, e registrar enquanto outra goroutine chama Criar é uma corrida
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)
        // SliceStable: entre itens de mesma relevância, mantém a ordem recebida
        sort.SliceStable(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 com interfaces: cada observador implementa ObservadorPedido
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()

    // Os observadores rodam depois que Publicar retorna, e o ctx de uma
    // requisição já estaria cancelado: WithoutCancel mantém os valores
    ctxObs := context.WithoutCancel(ctx)
    for _, o := range obs { // desde o Go 1.22, cada volta tem o seu o
        go func() {
            if err := o.Processar(ctxObs, 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)
}

// ReposicaoEstoque é o que o observador precisa; o RepositorioProduto não tem
// esse método, e uma interface pequena evita inchar a dele
type ReposicaoEstoque interface {
    ReporEstoque(ctx context.Context, pedidoID int64) error
}

type observadorEstoque struct{ repo ReposicaoEstoque }

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. A interface embutida fornece todos os métodos, que
// delegam ao repositório envolvido; só BuscarPorID é sobrescrito. Sem o
// embedding, a struct não satisfaria RepositorioProduto
type repoProdutoComLog struct {
    RepositorioProduto
    logger *slog.Logger
}

func NovoRepoProdutoComLog(repo RepositorioProduto, logger *slog.Logger) RepositorioProduto {
    return &repoProdutoComLog{RepositorioProduto: repo, logger: logger}
}

func (r *repoProdutoComLog) BuscarPorID(ctx context.Context, id int64) (*Produto, error) {
    inicio := time.Now()
    produto, err := r.RepositorioProduto.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}}
}

// Onde recebe a condição com ? no lugar de cada argumento. Cada ? vira $1,
// $2... na ordem de chegada, e uma condição opcional que fique de fora não
// desalinha a numeração das outras
func (b *BuilderConsulta) Onde(condicao string, args ...any) *BuilderConsulta {
    for _, a := range args {
        b.c.args = append(b.c.args, a)
        condicao = strings.Replace(condicao, "?", fmt.Sprintf("$%d", len(b.c.args)), 1)
    }
    b.c.condicoes = append(b.c.condicoes, condicao)
    return b
}

// OrderBy concatena o campo no SQL: nunca passe aqui texto vindo do usuário
// sem conferir contra uma lista de campos permitidos
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
func consultaEletronicos() (string, []any) {
    return NovaConsulta("produtos").
        Join("INNER JOIN categorias c ON c.id = produtos.categoria_id").
        Onde("produtos.ativo = ?", true).
        Onde("c.slug = ?", "eletronicos").
        OrderBy("produtos.preco", false).
        Limite(20).
        Offset(40).
        Construir()
}

Os padrões clássicos ficam menores em Go: Strategy vira uma função passada como parâmetro, Decorator é uma struct que embute a interface e sobrescreve um método, Repository e Observer são interfaces pequenas, e Factory e Builder resolvem a construção sem hierarquias. O valor de cada padrão está no problema que ele resolve, e não na forma do livro. As armadilhas também são as de Go: um tipo que não implementa a interface inteira, uma ordenação que não preserva empates, uma goroutine com um contexto que vai ser cancelado, um mapa compartilhado sem proteção. Um padrão bem aplicado ainda precisa de um teste que o atravesse.

Fontes e leituras recomendadas

Exercícios

Exercício 1

O decorator de logging da primeira versão deste artigo tinha um campo repo RepositorioProduto e só o método BuscarPorID. O NovoRepoProdutoComLog não compilava. Como o embedding resolve, e qual é a armadilha dele?

Ver resposta

✓ Resposta: Para satisfazer RepositorioProduto, a struct precisa ter os sete métodos, e a versão antiga tinha um. Ao embutir a interface, como em struct { RepositorioProduto; logger *slog.Logger }, a struct ganha todos os métodos da interface, que delegam ao valor embutido, e o BuscarPorID declarado na struct sobrescreve só esse. É o mesmo efeito das sete funções de delegação do decorator de cache, sem escrevê-las. A armadilha é que a delegação fica silenciosa: se alguém acrescenta um método à interface, o decorator continua compilando e passa a delegá-lo sem log nenhum, e um decorator de cache feito assim continuaria servindo dados velhos depois de um método novo de escrita que não invalida o cache. O mesmo mecanismo apareceu no handler de slog do artigo sobre logging, que perdia o request_id justamente porque métodos herdados do valor embutido escapavam do invólucro.

Exercício 2

A FactoryNotificador original registrava o e-mail com &notificadorEmail{host: cfg.Host, porta: cfg.Porta}. Em produção, o envio por e-mail falha com erro de autenticação no servidor SMTP, enquanto a função CriarNotificador funciona com a mesma configuração. Por quê?

Ver resposta

✓ Resposta: O construtor registrado copiava só o host e a porta, e o usuário e a senha ficavam vazios. Num teste com a mesma configuração, o registro antigo produziu {host:smtp.x porta:587 usuario: senha:}, e o novo, {host:smtp.x porta:587 usuario:u senha:s}. Havia duas fontes da verdade para criar o mesmo objeto, e elas divergiram; o registro antigo também pulava as validações, criando um notificador de SMS sem API key. A versão do artigo registra CriarNotificador para os três tipos: um só caminho de construção, com as validações, e no mesmo teste um SMS sem chave foi recusado com API key obrigatória para SMS. O registro dinâmico continua útil para tipos de fora do pacote, mas o padrão dos tipos internos não deve ter uma cópia própria da lógica.

Exercício 3

Uma busca por "caneca" deveria mostrar primeiro os produtos com essa palavra e, depois, os demais na ordem alfabética em que vieram do banco. Com a primeira versão de ordenarPorRelevancia, os demais aparecem embaralhados. O que estava errado?

Ver resposta

✓ Resposta: A função de comparação só distingue dois grupos, com e sem a palavra, e todos os itens de um mesmo grupo são "iguais" para ela. O sort.Slice não é estável: não promete manter a ordem original entre itens iguais, e o algoritmo os troca de lugar à vontade. Num teste com 40 itens em ordem alfabética, seis deles com "caneca", os itens que vieram depois das canecas começaram por 20 23 08 09 10 11. Com sort.SliceStable, a ordem recebida foi preservada: 01 02 03 04 05 06. A regra vale para qualquer ordenação por um critério parcial, como relevância, status ou prioridade: ou a ordenação é estável, ou a comparação inclui um critério de desempate, como o nome ou o ID. O pacote slices, mais novo, tem as mesmas duas opções: slices.SortFunc e slices.SortStableFunc.

Exercício 4

Com o builder original, que recebia os placeholders escritos à mão, alguém monta uma busca em que o filtro de status é opcional: Onde("ativo = $1", true) só entra se o usuário marcar a opção, e Onde("c.slug = $2", slug) entra sempre. O que acontece quando a opção fica desmarcada?

Ver resposta

✓ Resposta: A consulta sai com WHERE c.slug = $2 e um único argumento. Os placeholders do PostgreSQL são posicionais, e o $2 se refere ao segundo argumento, que não existe; o banco recusa a consulta por falta de parâmetro. O defeito só aparece na combinação de filtros que pula o primeiro, que é justamente a que o builder existe para montar. A versão do artigo recebe ? e numera na hora de acrescentar cada argumento: num teste com só a condição de slug, o resultado foi SELECT * FROM produtos WHERE c.slug = $1 com [eletronicos]. O builder também concatena o campo de OrderBy e os Join no texto do SQL, e esses nunca podem vir do usuário sem passar por uma lista de valores permitidos: placeholders protegem valores, e não nomes de colunas.

Exercício 5

O BusEventosPedido original passava o ctx recebido em Publicar direto para as goroutines dos observadores. Num handler HTTP que publica pedido.confirmado e responde, o e-mail de confirmação quase nunca sai. Explique e corrija.

Ver resposta

✓ Resposta: O contexto da requisição é cancelado quando o handler retorna, e as goroutines dos observadores começam a trabalhar depois disso. Qualquer chamada que respeite o contexto, como o envio do e-mail, falha com context canceled. É o mesmo defeito do e-mail de boas-vindas no artigo sobre injeção de dependência, e aparece em qualquer código que dispara trabalho assíncrono com o contexto de quem chamou. A versão do artigo cria, uma vez por publicação, context.WithoutCancel(ctx), que preserva os valores do contexto, como identificadores para log, sem o cancelamento; num teste que cancelava o contexto logo depois de publicar, o observador recebeu um contexto válido. Para não deixar uma goroutine presa indefinidamente, cada observador pode acrescentar o seu próprio WithTimeout. A versão antiga também tinha o := o antes da goroutine, uma cópia que deixou de ser necessária no Go 1.22, quando cada volta do for passou a ter a sua variável.

Comentários

Mais em Golang

Channels: comunicação entre goroutines
Channels: comunicação entre goroutines

Channels em Go: envio e recebimento, canais com e sem buffer, fechamento e…

Documentação com godoc e boas práticas de comentário
Documentação com godoc e boas práticas de comentário

Documentação em Go: comentários que começam pelo nome, cabeçalhos, listas e…

Compilação cruzada, binários estáticos e Docker com Go
Compilação cruzada, binários estáticos e Docker com Go

Compilação cruzada e Docker com Go: -ldflags e o que o debug.ReadBuildInfo já…