Go虽无类和继承,但可通过清晰职责边界实现高内聚:包名应精准反映单一职责(如auth),导出项语义一致,接口定义于调用方上游或独立contract包,避免隐式耦合与import循环。

Go 里没有“类”,但高内聚照样能做
Go 不支持传统 OOP 的继承和 private 成员,但这不等于没法实现高内聚。关键不是语法糖,而是职责边界是否清晰。一个包如果同时包含用户注册、密码重置、邮箱验证、第三方登录回调——哪怕全写在一个 user.go 文件里,也是低内聚;反过来,把密码策略、JWT 签发、OAuth2 客户端封装各自独立成包,哪怕只有 2–3 个函数,只要它们共同服务于“认证”这个单一目标,就是高内聚。
实操建议:
- 每个包名应直接反映其核心职责,比如
auth、payment、idgen,避免泛义词如util或common - 包内导出的类型/函数必须语义一致:若
auth包导出了NewJWTSigner和SendSMS,后者就该挪到sms包 - 测试文件(
_test.go)只测本包逻辑,不跨包调用具体实现——这是检验内聚是否到位的快速指标
接口定义位置决定依赖流向
Go 的接口是隐式实现的,但接口放在哪,决定了谁依赖谁。错误做法:把 Notifier 接口定义在 payment 包里,然后让 order 包 import 它——这等于订单模块强依赖支付模块,违反了依赖倒置。
正确做法是把接口定义在**调用方所在包的上游**,或者更常见的是:单独建一个 contract 或 port 包(如 internal/port),由业务模块(如 order)定义它需要什么能力,再由基础设施模块(如 payment)去实现。
立即学习“go语言免费学习笔记(深入)”;
实操建议:
- 接口名体现行为而非实现,比如
PaymentGateway比AlipayClient更合适 - 接口方法参数尽量用基础类型或本包定义的 DTO,避免引入其他包的 struct(否则又形成隐式耦合)
- 不要为“将来可能扩展”而提前定义大而全的接口,先按当前最小契约设计,够用就好
import 循环报错时,先别急着拆包
import cycle not allowed 是 Go 编译器最直白的警告,但它常被误读为“包拆太细”。其实多数情况是:两个包在互相调用具体实现,而不是通过接口通信。
比如 order 包直接调用了 payment 包的 ChargeCard 函数,而 payment 又调用了 order 包的 GetOrderStatus —— 这不是包结构问题,是设计问题。
实操建议:
- 用
go list -f '{{.Deps}}' ./pkg/name查看真实依赖链,确认是不是真有循环 - 检查所有跨包调用:是否都在用接口?有没有漏掉某个直接 import struct 或函数的地方?
- 临时加一层中间包(如
event)转发消息,比强行合并包更安全——事件本质就是解耦的天然载体
go.mod 中 replace 和 indirect 不代表耦合低
replace 能让你本地改依赖不影响 CI,indirect 表示某模块被间接引用——但这两者都不改变实际代码里的耦合强度。你完全可以在 go.mod 里干净地管理版本,却在 order/service.go 里硬编码调用 payment.db.Query。
真正衡量耦合度的,是源码中是否出现跨包的具体类型、方法调用、或全局变量引用。
实操建议:
- grep -r '\.db\.' ./internal/order/ 这类命令比看
go.mod更能暴露耦合点 - CI 中加入静态检查:比如用
golangci-lint配置goimports+errcheck,但更重要的是自定义规则禁止特定包路径出现在不该出现的地方 - 定期执行
go mod graph | grep -E "(order|payment)",观察依赖方向是否符合你画的架构图
最难的不是写接口,而是忍住不在接口里塞“方便以后加功能”的字段或方法;最隐蔽的耦合,往往藏在 error 类型判断、context key 定义、甚至 log 字段命名里——这些地方没 import,但一样会锁死模块演进。


















