模块拆分应从剥离main.go职责开始,仅保留配置解析、日志初始化和HTTP服务启动,其余逻辑外移至internal各子包,并通过Go Modules+接口抽象实现真正解耦。

模块拆分要从 main.go 的职责剥离开始
Go Web 项目初期常把路由注册、中间件、DB 初始化全塞进 main.go,结果越写越难维护。真正该留在 main.go 的只有程序入口逻辑:解析配置、初始化日志、启动 HTTP 服务。其余全部外移。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
-
main.go只调用app.Run()或类似封装函数,不写任何 handler 或 DB 操作 - 路由定义统一收口到
internal/route包,用函数返回http.ServeMux或gin.Engine实例 - 数据库初始化逻辑放进
internal/infra/db,导出NewDB(),由主函数传参或依赖注入 - 避免在
main.go中直接 importinternal/handler/user这类业务包——它只应依赖internal/app这类抽象层
用 Go Modules 管理跨模块依赖而非目录硬引用
很多人以为“建个 internal/user 目录就算模块化”,但若 internal/order 直接 import internal/user/service.go,实际仍是紧耦合。真正的模块边界靠 Go Modules + 接口抽象来维持。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每个业务域(如 user、order)单独建一个
go.mod,例如internal/user/go.mod,module 名为example.com/internal/user - 对外暴露的类型必须是接口,定义在
internal/user/port下,比如UserRepository接口 - 具体实现(如 GORM 版本)放在
internal/user/adapter/gorm,不被其他模块 import,只在main.go或internal/app中组合 - 运行
go mod tidy时若提示 “require internal/xxx: version is required”,说明模块未正确发布或路径未被主模块识别,需检查replace或go.work
http.HandlerFunc 不要跨模块直接传递,用中间件链或 Router 分组替代
常见错误是 A 模块导出一个 func(http.ResponseWriter, *http.Request),B 模块直接注册到自己的 mux 上,导致 handler 依赖 A 的内部结构(如未导出字段、私有工具函数),破坏封装。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每个模块提供自己的
RegisterRoutes(*mux.Router)函数,接收标准 router 接口(如gorilla/mux.Router或gin.IRouter),内部完成路径前缀、中间件绑定、handler 注册 - 公共中间件(鉴权、日志)统一放在
internal/middleware,通过函数选项模式传参,例如AuthMiddleware(roles ...string) - 避免在 handler 内部调用另一个模块的 service 方法——应通过 domain event 或 message bus 解耦,比如用户创建后发
UserCreatedEvent,订单模块监听处理 - 如果用 Gin,别在
user.RegisterRoutes(r.Group("/api/v1"))里硬编码版本号;改用r.Group("/api", versionMiddleware("v1"))统一控制
测试时模块隔离的关键是接口 + Mock + testmain
模块化后单元测试容易卡在“测不到真实依赖”上。比如 user.Service 依赖 user.Repository,而后者连 DB。这时候不是绕过测试,而是让测试只关心契约。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 每个模块的
service层测试只 import 本模块和mock包,不 importadapter或infra - 用
gomock或testify/mock生成UserRepositoryMock,断言调用次数与参数,不验证 SQL 是否执行 - 集成测试放
internal/app/e2e,用testmain启动最小依赖栈(内存 DB、stub HTTP client),验证模块间协作是否符合预期 - 禁止在
user/service_test.go里写os.Setenv("DB_URL", "...")—— 配置应通过构造函数注入,测试时传入 mock 或内存实例
模块化不是给目录起好听的名字,而是让每个 go.mod 能独立编译、测试、替换。最容易被忽略的是:接口定义的位置必须比实现早,且不能随实现变;否则今天加个 UpdatedAt 字段,所有模块都得改。先想清楚“这个模块对外承诺什么”,再写代码。










