单体应用拆分应以业务限界上下文为依据,而非技术层;每个上下文对应独立目录(如internal/order),包内只处理本域事务,跨域交互必须通过明确定义的接口(如payment.service),internal与pkg职责分离,模块间依赖需显式契约化且由main.go组合实现。

单体应用不是不能跑,而是改一行代码要测全链路、加个新功能得协调五个人——拆模块不是为了炫技,是让每次 git push 都更可控。
按业务限界上下文切分模块,别按技术层
很多人一上来就建 controller、service、repository 三个包,结果所有业务逻辑挤在 service 里,越长越像意大利面。真正的边界来自业务语言,比如“订单”“支付”“用户中心”,而不是“HTTP 层”或“DB 层”。
- 每个限界上下文对应一个独立目录,如
internal/order、internal/payment - 包内类型和函数只处理本域事务,不暴露跨域状态(例如
order.Order不含payment.Status字段) - 跨域交互必须走定义好的接口,比如
payment.Service提供Charge(ctx, orderID),而非直接读取订单表
internal 和 pkg 的分工必须清晰
internal/ 是你的“内部器官”,别人不能 import;pkg/ 是你愿意对外亮出来的“API 能力”。混淆这两者,等于把数据库密码写在 README 里。
-
internal/order可以自由调用internal/user,但只能通过user.LookupByID()这类封装函数,不能直接访问user.db或user.cache -
pkg/util放通用工具(日志包装、重试策略),但禁止放业务逻辑;一旦发现pkg/util/order_helper.go,立刻删掉并移到internal/order -
go list -f '{{.ImportPath}}' ./internal/...应该完全不包含pkg的路径,反之亦然
模块间通信必须显式依赖,拒绝隐式耦合
Go 没有“服务发现”概念,也没有运行时注入——模块怎么知道对方存在?靠 import。但 import 不等于随便连,得有契约。
- 跨模块调用必须通过 interface 定义,比如
payment.Service接口由internal/payment实现,但被internal/order依赖 - 实现方包名和接口包名不能相同(避免循环 import),建议接口放在
pkg/payment,实现放在internal/payment - 启动时才组合实现,比如
main.go创建payment.NewService(...)并传给order.NewService(...),而不是让order包自己 new
go.mod 不是装饰品,是模块边界的锚点
一个项目只有一个 go.mod,但它决定了哪些路径算“本模块”,哪些算“外部依赖”。误配会导致 go build 找不到包,或测试时 mock 失败。
-
module example.com/myapp写在go.mod里,所有internal/和pkg/下的包导入路径都必须以它开头,比如example.com/myapp/internal/order - 如果想把
payment拆成独立仓库,先把它从internal/移到pkg/,再发布 tag,最后在主项目go.mod中require example.com/payment v1.2.0 -
go mod tidy后检查go.sum是否新增了不该出现的哈希——如果有,说明某个包偷偷 import 了外部模块
最难的不是拆,是决定哪部分先不动。比如用户认证逻辑牵扯登录、权限、token 刷新,强行拆开反而增加调用延迟;不如先把它封进 internal/auth,等订单模块稳定后再抽离 token 校验为独立服务。边界感比结构图重要得多。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











