O Poder do Functional Options Pattern

24 de abril de 2026
O Poder do Functional Options Pattern

Se tem uma coisa que eu aprendi tomando porrada em produção é que código rígido quebra fácil. No começo da minha carreira, eu adorava criar construtores gigantes. Se eu tinha uma série de variáveis para iniciar um servidor, o NewServer recebia endereço, timeout, porta, certificado... e por aí vai.

O problema? Toda vez que surgia uma configuração nova, eu tinha que alterar a assinatura da função e quebrar o código de todo mundo que já usava aquela função. Ou pior: acabava criando aquelas aberrações como NewServerWithTimeout, NewServerDefault e por aí vai. Você também já sentiu esse peso de manter um código que parece um castelo de cartas?

Hoje, quero te apresentar uma das formas mais elegantes e idiomáticas de resolver isso em Go: o Functional Options Pattern.

A Analogia do Restaurante à La Carte

Imagine que você está em um restaurante. O jeito "antigo" (e rígido) de configurar um objeto seria como um Prato Feito: você recebe o que está no cardápio e, se quiser trocar o arroz por fritas, o garçom entra em curto-circuito porque o sistema não permite.

O Functional Options é como um Buffet à La Carte. O restaurante te entrega uma base (os valores default) e você escolhe exatamente o que quer adicionar ou modificar, sem bagunçar a cozinha.

O Conceito Central

Em vez de passar dez parâmetros, passamos uma lista variável (variadic) de funções. Cada função sabe exatamente como modificar um campo da sua struct.

  • A Option: Definimos um tipo type Option func(*Server).
  • O Construtor: O NewServer(opts ...Option) inicializa os valores padrão e depois "itera" sobre as opções aplicando as mudanças.

Colocando a Mão na Massa

Vamos imaginar o cenário de um servidor HTTP. Ele tem configurações padrão, mas às vezes você precisa de algo cirúrgico:

package main

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

// Server representa um HTTP server configurável.
type Server struct {
    Addr     string
    Timeout  time.Duration
    TLS      bool
    Logger   *log.Logger
    MaxConns int
}

// Option é o tipo função para aplicar configurações.
type Option func(*Server)

// DefaultAddr porta padrão HTTP.
const DefaultAddr = ":8080"

// DefaultTimeout timeout padrão.
const DefaultTimeout = 30 * time.Second

// DefaultMaxConns conexões máximas padrão.
const DefaultMaxConns = 100

var (
    defaultLogger = log.New(log.Writer(), "server: ", log.LstdFlags)
)

// WithAddr define o endereço de escuta, com validação.
func WithAddr(addr string) Option {
    return func(s *Server) {
        if addr == "" {
            panic("addr cannot be empty")
        }
        s.Addr = addr
    }
}

// WithTimeout define o timeout, com validação.
func WithTimeout(timeout time.Duration) Option {
    if timeout <= 0 {
        panic("timeout must be positive")
    }
    return func(s *Server) {
        s.Timeout = timeout
    }
}

// WithTLS habilita TLS.
func WithTLS(tls bool) Option {
    return func(s *Server) {
        s.TLS = tls
    }
}

// WithLogger define o logger customizado.
func WithLogger(l *log.Logger) Option {
    return func(s *Server) {
        s.Logger = l
    }
}

// WithMaxConns define conexões máximas.
func WithMaxConns(max int) Option {
    return func(s *Server) {
        if max <= 0 {
            max = DefaultMaxConns
        }
        s.MaxConns = max
    }
}

// NewServer cria um server com defaults + opções.
// Retorna erro se configuração inválida.
func NewServer(opts ...Option) (*Server, error) {
    s := &Server{
        Addr:     DefaultAddr,
        Timeout:  DefaultTimeout,
        Logger:   defaultLogger,
        MaxConns: DefaultMaxConns,
    }

    for _, opt := range opts {
        opt(s)
    }

    // Validações finais
    if s.Addr == "" {
        return nil, fmt.Errorf("server addr cannot be empty")
    }
    if s.Timeout <= 0 {
        return nil, fmt.Errorf("server timeout must be positive")
    }

    return s, nil
}

// Start inicia o server (exemplo de uso).
func (s *Server) Start() error {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hello, Go!")
    })

    srv := &http.Server{
        Addr:         s.Addr,
        Handler:      mux,
        ReadTimeout:  s.Timeout,
        WriteTimeout: s.Timeout,
        MaxHeaderBytes: 1 << 20, // 1MB
    }

    s.Logger.Printf("Server starting on %s (TLS: %v, MaxConns: %d)", s.Addr, s.TLS, s.MaxConns)

    if s.TLS {
        return srv.ListenAndServeTLS("cert.pem", "key.pem")
    }
    return srv.ListenAndServe()
}

func main() {
    srv, err := NewServer(
        WithAddr(":3000"),
        WithTimeout(10*time.Second),
        WithMaxConns(500),
        WithLogger(log.New(log.Writer(), "myapp: ", log.LstdFlags)),
    )
    if err != nil {
        log.Fatal(err)
    }
    if err := srv.Start(); err != nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
}

Para usar algo simples, a leitura fica quase como uma frase em inglês: srv := NewServer(WithAddr(":3000"), WithTimeout(10*time.Second)) e se você precisa de algo robusto poderia usar um srv := NewServer(WithAddr(":3000"), WithTimeout(10*time.Second), WithMaxConns(500), WithLogger(log.New(log.Writer(), "myapp: ", log.LstdFlags))).

Por que usar esse padrão no seu PR de hoje?

Se você está na dúvida se vale o "boilerplate" extra, deixo aqui os pontos que me convenceram ao longo dos anos:

  • Extensibilidade Infinita: Se amanhã o time de segurança exigir um MaxConnections, eu só crio a função WithMaxConns (que no caso já existe). O NewServer continua com a mesma cara e não quebra nenhum main.go legado.
  • Código Autodocumentado: No código que consome, você lê WithTimeout. É muito mais claro do que passar um 30 perdido no meio de cinco inteiros.
  • Segurança e Validação: Dentro da própria função de opção, você pode colocar logs ou validações. Se o endereço for vazio, você já trata ali mesmo.
  • Zero Valor Opcional: Você não precisa passar nil ou "" para campos que não quer configurar. Você simplesmente não passa a opção.

Quando dar um passo atrás?

Não seja um "purista de padrões". Se a sua struct é interna, pequena e nunca vai mudar, uma struct de configuração simples resolve bem. O Functional Options brilha quando você está construindo bibliotecas, SDKs ou pacotes core que serão usados por muita gente. É uma forma de você, como desenvolvedor experiente, garantir que o seu "leitor futuro" tenha uma vida fácil.

No fim do dia, escrever código é sobre reduzir o atrito. É sobre garantir que, se alguém precisar mudar uma configuração na sexta-feira às 17h, essa pessoa não precise reescrever metade do sistema.

E você? Já teve que lidar com construtores que pareciam formulários de imposto de renda de tão grandes? Como você resolveu isso na época?