Refatoração de código legado em Go: guia passo a passo

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

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

Blogs

Book Recommendations