Go中实现高内聚低耦合依赖包边界、接口契约与依赖注入三者协同:包按单一职责物理隔离,接口由调用方定义且小而精,依赖在启动时显式注入,避免隐式耦合与职责混杂。

Go 语言里没有“高内聚低耦合”的魔法开关,它靠的是包边界 + 接口契约 + 依赖注入这三件套落地。不按这个组合来,光靠目录分层或命名规范,模块照样会越长越胖、越改越脆。
包划分必须遵循物理路径即语义边界
Go 的 import 路径就是模块的唯一标识,不是随便建个 pkg/xxx 目录就能叫“模块”。一个包对外暴露什么,只由它的导出符号(首字母大写的类型、函数、变量)决定,和目录名无关。
- 避免把多个职责塞进同一个包,比如
user包里混着数据库操作、HTTP handler、校验逻辑——这会让包内部高耦合,外部调用却不得不引入一堆无关依赖 - 每个包应有单一清晰的职责,例如
user/domain只放User结构体和领域方法;user/repo只实现UserRepository接口;user/api只处理 HTTP 映射 - 跨包调用必须显式
import,不能靠“同级目录就自动可见”这种错觉——Go 不支持子包隐式继承父包符号
接口定义要小而具体,且由调用方声明
接口不是写在被调用方包里用来“暴露能力”的,而是由调用方根据自身需要抽象出来的契约。这是 Go 接口设计最容易翻车的地方。
- 别在
payment包里定义PaymentService接口然后让order包去实现它——这会让订单逻辑被迫适配支付模块的结构 - 应该由
order包定义Notifier接口(如NotifyOrderPaid(orderID string) error),再让payment包提供满足该接口的具体类型 - 接口方法越少越好,一个接口只表达一种能力。比如
Reader和Writer比ReadWriteCloser更易复用、更易 mock
依赖注入必须在应用启动时完成,而非运行时动态查找
Go 没有运行时 DI 容器,所谓“注入”,本质是把具体实现传给依赖它的结构体字段或函数参数。关键在于时机和方式。
立即学习“go语言免费学习笔记(深入)”;
- 不要在 handler 函数里 new 一个
&UserServiceImpl{},这等于硬编码依赖,测试时无法替换 - 应在
main()或初始化函数中构造完整依赖树,例如:userService := &UserService{repo: mysqlRepo, validator: userValidator} - Gin 等框架的中间件或路由闭包里,应把已构造好的服务实例传进去,而不是每次请求都重建
- 如果服务之间存在循环依赖(A 依赖 B,B 又依赖 A),说明职责划分错了——得回退一步重新切分包和接口
真正难的从来不是写接口或分目录,而是判断“这个逻辑到底该属于谁”。比如一个用户注册流程里发短信、存 DB、写日志,这三个动作谁该负责协调?答案是:新建一个 registration 包,它依赖 sms.Sender、user.Repo、log.Logger 这三个接口,自己不实现任何具体逻辑——这才是高内聚低耦合的落点。


















