Go模块本身不保证高内聚低耦合,真正起作用的是包组织方式、接口定义位置及跨模块依赖方向控制;模块边界须对齐业务语义,禁止跨模块直接导入实现,应通过顶层接口+依赖注入解耦。

直接说结论:Go 模块(go mod)本身不保证高内聚低耦合,它只是依赖管理的容器;真正起作用的是包(package)的组织方式、接口定义位置、以及跨模块调用时的依赖方向控制。
模块边界 ≠ 包边界,但必须对齐业务语义
很多人误以为只要每个业务域建一个 go module(比如 github.com/org/order、github.com/org/payment)就自然解耦了。实际中常见错误是:模块内包结构仍是扁平的 controller/、service/、dao/,导致业务逻辑被撕裂到多个包里,违反高内聚。
- 正确做法:每个模块根目录下只暴露一个顶层
package(如order),内部按领域分层(domain/、application/、infrastructure/),但对外仅通过该顶层包提供接口 - 模块间禁止直接 import 对方的
internal/或具体实现包(如payment/internal),只能依赖其api/或contract/中声明的 interface - 如果模块间需共享类型(如
OrderID),应单独抽成github.com/org/common模块,并设为只读、无业务逻辑
跨模块调用必须通过 interface + 依赖注入
常见错误现象:order 模块直接 import payment 的具体实现(如 payment/service),导致编译期强依赖,改支付渠道就得重编译订单模块。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 支付模块应导出
payment.Notifier接口,定义在payment/api.go中,且该文件所在包为payment(非payment/internal) - 订单模块的
Complete()方法接收notifier payment.Notifier参数,而非*payment.Service - 具体实现(如
wechat.PaymentService)放在独立模块或cmd/下组装,用 DI 容器或手动注入 - 避免循环依赖:
user可依赖common,但common绝不能 importuser或任何业务模块
go.mod 文件里的 replace 和 require 要反映真实依赖流向
开发阶段常用 replace 指向本地路径,但容易掩盖依赖方向问题——比如把 order 的 replace github.com/org/payment => ./payment 写在 order/go.mod 里,看似方便,实则让 order 模块“知道” payment 的存在路径,违背了依赖倒置原则。
立即学习“go语言免费学习笔记(深入)”;
-
replace应只出现在根项目(如cmd/server/go.mod)中,用于临时覆盖所有子模块的版本 - 各业务模块的
go.mod中require列表只保留真正需要编译的依赖(如github.com/google/uuid),绝不出现其他业务模块(github.com/org/payment不能出现在order/go.mod中) - 构建失败提示 “import cycle not allowed” 是最诚实的警告,别用
//nolint忽略它
最容易被忽略的一点:模块名和 import path 的稳定性比代码逻辑更难维护。一旦 github.com/org/order 改成 github.com/org/oms/order,所有依赖它的模块都要同步改 import 和 go.mod,而这种变更无法被 IDE 自动识别。所以模块拆分要慎重,优先用包内重构,不到万不得已不动模块边界。

















