Go 遗留代码重构入门
处理遗留代码是每位 Go 开发者职业生涯中不可避免的一部分。随着时间的推移,项目会积累技术债务:出现单体函数、代码重复、缺乏测试和错误处理。Go 遗留代码的重构不仅仅是重写,而是一个在不改变系统外部行为的前提下改善架构的战略性过程。
在本文中,我们将分析 Go 项目重构的主要方法,提供具体的代码示例,并展示如何让旧的代码库变得更干净、更安全、性能更高。
为什么 Go 遗留代码需要特殊方法?
Go 以其简洁和严谨著称,但即使如此,它也会积累问题:
- 缺少接口 — 组件之间高度耦合;
- 忽略错误 — 直接 return 而不检查 err;
- 全局变量 — 隐式依赖;
- 逻辑重复 — 复制粘贴而非组合;
- 长函数 — 超过 50 行且承担多项职责。
没有测试的重构就像没有麻醉的手术。因此,第一步是确保关键部分的测试覆盖率。
重构策略:从单体到模块
1. 提取接口与依赖反转
我们从典型的遗留代码开始,其中业务逻辑与存储实现紧密耦合:
// 重构前type 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}问题:没有真实的数据库就无法测试这样的代码。解决方案是引入接口:
// 重构后type 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)}现在我们可以轻松地在测试中将实现替换为 mock。
2. 拆分大函数(单一职责原则)
典型的遗留代码——一个函数完成所有事情:
// 重构前func processOrder(order Order) error { // 验证 if order.Amount <= 0 { return errors.New("invalid amount") } // 计算折扣 discount := 0.0 if order.CustomerType == "vip" { discount = 0.1 } // 保存到数据库 _, err := db.Exec("INSERT INTO orders ...") if err != nil { return err } // 发送邮件 return sendEmail(order.Email, "Order confirmed")}重构后,我们将其拆分为小的、可测试的函数:
// 重构后type 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. 用依赖注入替换全局变量
全局变量是测试的头号敌人。