模块边界应按数据主权和变更节奏划分,而非数据库表或目录名;internal/用于内部可见性隔离,pkg/用于对外契约发布;跨模块通信须通过接口抽象和事件驱动,禁止直连内部实现。

模块边界该按什么划?别碰数据库表和目录名
模块边界不是由“user表”或“order目录”决定的,而是由谁对某个业务概念拥有数据主权、变更节奏是否一致来定。比如“用户登录”和“用户实名认证”,如果登录逻辑由风控团队维护、实名由合规团队维护,且上线时间差两周,就该拆成两个模块——哪怕它们都读写同一张 user 表。
常见错误现象:internal/user 包里混着注册、登录、实名、头像上传,结果改个短信验证码逻辑,要同步更新四个 handler、三个 service、两个 repo,还容易漏掉某处缓存失效逻辑。
- 画出核心流程图,标出每个环节的负责人、SLA、外部依赖(如风控 API、短信网关)
- 频繁一起修改、强一致要求、共享事务边界的功能,保留在同一模块
- 仅通过事件交互、允许最终一致、部署节奏不同的功能,必须拆出独立模块
- 初期可共用 PostgreSQL 实例,但必须用不同
schema,禁止JOIN跨 schema 查询
internal/ 和 pkg/ 怎么用才不踩坑
internal/ 是 Go 工具链强制的可见性隔离机制,不是命名习惯;pkg/ 是对外提供契约的公共能力出口,不是“工具函数大杂烩”。两者混用会导致模块边界瞬间失效。
典型误用:pkg/common 里塞了日志封装、错误码、HTTP 工具、UUID 生成器,结果 order 模块 import pkg/common,user 模块也 import,半年后发现 pkg/common/log 依赖了某个只有 order 才用的中间件,一升级就崩。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
-
internal/config、internal/middleware、internal/server可被多个 cmd 共用,但绝不允许外部项目 import -
pkg/logger、pkg/httpx、pkg/uuid必须有完整单元测试、文档注释、语义化版本 tag - 一旦发现
pkg/下某个包被 3 个以上业务模块强依赖,立刻评估是否已演变成“上帝包”,考虑按场景拆分(如pkg/logger→pkg/logger/zap+pkg/logger/sentry) - 新增包后立即运行
go mod tidy,若提示require internal/xxx: version is required,说明路径未被主模块识别,检查go.work或 replace 是否滥用
跨模块调用为什么不能直接 import service?
直接 import "xxx/internal/order/service" 看似方便,实则把实现细节、错误类型、数据库模型全暴露出去,等于把模块耦合钉死在编译期。改 order 的一个字段类型,所有调用方都要重编译、重测、重发。
真实问题场景:用户创建成功后要触发风控扫描、发站内信、更新统计看板。如果在 user/service.go 里硬编码调用 risk.Scan()、notify.Send()、analytics.Inc(),那 user 模块就承担了协调职责,违反单一职责,也锁死了其他模块的演进自由。
- user 模块只发布
UserCreatedEvent,其他模块各自监听处理 - 所有跨模块通信走接口抽象:定义
PaymentProcessor接口,由 grpcPaymentProcessor 或 mockPaymentProcessor 实现,启动时注入 - 禁止出现
import "xxx/internal/order/repo"这类直连内部实现的写法 - 若某个包 import 超过 3 个其他业务包,说明它已变成协调中心,该拆或重定义边界
proto 接口定义最容易翻车的三个地方
gRPC + Protobuf 不是“写完 .proto 就能跑”,契约一旦发布,修改成本极高。很多团队上线第一周就得返工,就是因为 proto 写得太随意。
错误示例:rpc GetUserWithOrders(GetUserRequest) returns (GetUserResponse) —— 这等于把订单聚合逻辑强绑在用户服务契约上,下游一要加收货地址字段,你就得改 proto、发版、等所有客户端升级。
- message 字段全部加
optional(proto3 默认),新增字段不破坏旧客户端 - 错误统一用
status.Code(如codes.NotFound),不传自定义 error 字符串 - 每个服务只维护自己的
proto/目录,不共用 mega-proto 仓库 - 服务启动时校验 proto 版本兼容性,可用
protoc-gen-validate或自定义插件
模块拆分最易被忽略的点:不是目录怎么建,而是每次新增一个 import、每写一行跨模块调用、每改一个 proto 字段,都在悄悄侵蚀边界。边界不是设计出来的,是靠每次代码审查守住的。

















