Introdução à Refatoração de Código Go Legado
Trabalhar com código legado é uma parte inevitável da vida de qualquer desenvolvedor Go. Com o tempo, os projetos acumulam dívida técnica: funções monolíticas, duplicação de código, falta de testes e tratamento inadequado de erros. Refatorar código Go legado não é apenas reescrever, mas um processo estratégico de melhoria da arquitetura sem alterar o comportamento externo do sistema.
Neste artigo, vamos detalhar as principais abordagens para refatorar projetos Go, fornecer exemplos específicos de código e mostrar como tornar uma base de código antiga mais limpa, segura e com melhor desempenho.
Por que o Código Go Legado Exige uma Abordagem Especial?
Go é conhecido por sua simplicidade e rigidez, mas mesmo em Go, problemas se acumulam:
- Falta de interfaces — acoplamento forte entre componentes;
- Ignorar erros — retornar sem verificar err;
- Variáveis globais — dependências implícitas;
- Duplicação de lógica — copiar e colar em vez de composição;
- Funções longas — mais de 50 linhas com múltiplas responsabilidades.
Refatorar sem testes é como cirurgia sem anestesia. Portanto, o primeiro passo é garantir cobertura de testes para áreas críticas.
Estratégia de Refatoração: Do Monólito aos Módulos
1. Extraindo Interfaces e Inversão de Dependência
Vamos começar com um código legado típico onde a lógica de negócio está fortemente acoplada à implementação de armazenamento:
// Antes da refatoraçãotype UserService struct { db *sql.DB}
func (s *UserService) GetUser(id int) (*User, error) { row := s.db.QueryRow("SELECT id, name FROM users WHERE id = ?", id) var u User err := row.Scan(&u.ID, &u.Name) return &u, err}O problema: esse código não pode ser testado sem um banco de dados real. A solução é introduzir uma interface:
// Depois da refatoraçãotype Storage interface { GetUser(id int) (*User, error)}
type UserService struct { storage Storage}
func NewUserService(storage Storage) *UserService { return &UserService{storage: storage}}
func (s *UserService) GetUser(id int) (*User, error) { return s.storage.GetUser(id)}Agora podemos substituir facilmente a implementação por um mock nos testes.
2. Dividindo Funções Grandes (Responsabilidade Única)
Código legado típico — uma função que faz tudo de uma vez:
// Antes da refatoraçãofunc processOrder(order Order) error { // Validação if order.Amount <= 0 { return errors.New("invalid amount") } // Cálculo de desconto discount := 0.0 if order.CustomerType == "vip" { discount = 0.1 } // Salvar no banco _, err := db.Exec("INSERT INTO orders ...") if err != nil { return err } // Enviar e-mail return sendEmail(order.Email, "Order confirmed")}Após a refatoração, dividimos em funções pequenas e testáveis:
// Depois da refatoraçãotype OrderProcessor struct { db Database notifier Notifier}
func (p *OrderProcessor) Process(order Order) error { if err := validateOrder(order); err != nil { return err } order = applyDiscount(order) if err := p.db.SaveOrder(order); err != nil { return fmt.Errorf("save order: %w", err) } return p.notifier.SendConfirmation(order.Email)}
func validateOrder(order Order) error { if order.Amount <= 0 { return errors.New("amount must be positive") } return nil}
func applyDiscount(order Order) Order { if order.CustomerType == "vip" { order.Amount *= 0.9 } return order}3. Substituindo Variáveis Globais por DI (Injeção de Dependência)
Variáveis globais são o principal inimigo do teste