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çãoWithMaxConns(que no caso já existe). ONewServercontinua com a mesma cara e não quebra nenhummain.golegado. - Código Autodocumentado: No código que consome, você lê
WithTimeout. É muito mais claro do que passar um30perdido 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
nilou""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?
