go项目重构核心是解决handler业务臃肿、层间职责模糊、错误处理散乱、测试依赖难隔离等真实痛点,需通过按业务域组织包结构、小接口解耦、dto校验分离、补全测试并保障错误类型稳定来实现。

Go 框架开发中,代码重构不是为了“看起来更高级”,而是解决真实痛点:handler 里塞满业务逻辑、service 层和 repository 层职责模糊、错误处理散落在各处、测试无法隔离依赖。重构有效与否,取决于是否让 cmd/ 更薄、internal/domain/ 更稳、api/ 更纯。
按业务域组织包结构,而非技术分层
把所有 handler 放进 handlers/、所有 model 放进 models/ 是 Go 项目早期最常见的结构陷阱。它导致跨模块引用随意、职责边界模糊、迁移成本高。
- 每个业务模块(如
user、order)应自包含:接口定义(interface.go)、HTTP handler(只依赖本模块接口)、核心 service(不直接 import db 或 http 包)、repository 接口(repository.go)和单元测试 - 禁止跨模块直接引用实现,例如
user/internal/service.go不得 importorder/internal/repository;如需协作,通过定义在user/interface.go中的回调函数或事件接口通信 - 公共基础能力(日志、配置、HTTP 工具)统一收口到
internal/pkg/,并显式导入;避免用_空导入或全局变量初始化
用小接口解耦依赖,拒绝“大而全”的 service 接口
定义一个 UserService 接口,里面塞了 Create、Update、Delete、SendEmail、ExportCSV —— 这类接口难实现、难 mock、难测试,也违背 Go 的“小接口哲学”。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 按调用方视角拆分:HTTP handler 只需要
UserCreator和UserFinder;后台任务可能只需要UserExporter - 接口名体现用途而非实体,例如
type UserNotifier interface { Notify(user *User, msg string) error },而不是type UserService interface { ... } - 实现时可组合多个小接口:
type userSvc struct{ creator UserCreator; finder UserFinder; notifier UserNotifier },而非继承一个巨接口
提取校验与转换逻辑为独立函数,避免 handler 胀大
Gin handler 里写 c.ShouldBindJSON(&req) + 多重 if req.X == "" + strings.TrimSpace + time.Parse 是典型坏味道。它让 handler 同时承担协议解析、数据校验、格式转换、业务路由四重职责。
- 为每个 API 请求体定义专属 DTO(放在
api/v1/下),并配套Validate()方法(返回error,不 panic) - 校验失败时统一用
c.Error(errors.New("invalid request"))或封装为ErrorResponse,避免在 handler 内部写return c.JSON(400, ...) - 时间、金额、ID 等格式转换逻辑抽成私有函数,例如
parseOrderTime(s string) (time.Time, error),便于复用和测试
重构前必须补全测试,否则就是在赌运气
没有测试覆盖的重构,等价于在生产环境改 SQL 语句却不跑 explain。尤其对 Gin handler 和 service 层,测试缺失意味着你根本不知道“外部行为不变”这个前提是否成立。
- handler 测试用
gin.CreateTestContext+httptest.NewRequest,验证状态码、响应体、错误类型,**不验证具体 JSON 字段值**(那是 DTO 层职责) - service 测试用接口 mock(如
gomock或 hand-written fake),重点覆盖分支路径(如用户不存在、库存不足、支付超时) - 重构前先运行
go test -v -race ./internal/domain/user/...,确认原有测试全部通过;重构后再次全量运行,且新增覆盖率不低于 85%
最易被忽略的是错误类型的稳定性:errors.Is(err, ErrUserNotFound) 这类判断一旦在重构中被改成 fmt.Errorf("user %d not found", id),上游所有错误分类逻辑就失效了。接口契约比实现更重要。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










