单体化混合架构是阶段性演进的务实选择,golang模块划分应基于业务变更频率而非技术分层,需通过git log识别高频修改目录合并为同一module,跨域proto/openapi需先收敛契约,禁止internal跨module引用,replace用于本地开发与线上部署路由切换,跨module调用必须经显式契约(如payment/api),错误须封装为带code()方法的自定义类型,utils复用应延迟抽象、先复制再收敛。

单体化混合架构不是折中妥协,而是阶段性演进的务实选择——Golang模块分配必须服务于“哪些代码能先拆、哪些必须共存、哪些要预留接口”,而不是按目录结构强行切分。
module边界必须由业务变更频率决定,而非技术分层
很多团队一上来就按handler/service/dao建包,结果用户服务和订单服务共享同一个pkg/db,改个MySQL驱动全量回归。真实约束来自业务:如果“患者信息”和“知情同意书”总是一起修改、一起发布,它们就该在同一个go module里;但“电子日志采集规则”每两周迭代一次,就必须独立成edc-rules模块,哪怕初期只有一两个函数。
- 检查
git log --oneline -n 20 -- pkg/,高频提交的目录优先合并为同一module - 对外暴露的
proto或openapi.yaml若跨多个逻辑域,说明边界已模糊,需先收敛契约再拆module - 避免
internal/下跨module引用——Golang会报import "xxx/internal" is not allowed,但更危险的是隐式耦合:比如user/internal/cache被order服务直接import,表面编译通过,实则破坏自治性
gomod replace不是临时补丁,而是混合架构的路由开关
当patient-service还在单体里跑,但study-service已独立部署时,用replace让两者在本地开发时走本地路径、上线时走远程grpc,比硬编码if dev { local } else { remote }干净得多。
- 在根
go.mod中写:replace github.com/org/patient => ./internal/patient - 上线前CI自动删掉
replace行,并确保go build -mod=readonly失败时阻断发布 - 禁止在
replace目标路径里放main.go——它必须是纯库,否则go test会误执行入口逻辑
跨module调用必须经过显式契约,哪怕是同进程
即使所有服务跑在一个二进制里,order模块调用payment也绝不能import "github.com/org/payment/internal"。这会让未来拆分变成一场灾难:你得重写所有import路径、重构测试桩、重新设计错误传播链。
- 定义
payment/api包,只暴露Pay(ctx context.Context, req *PayReq) (*PayResp, error)这类函数,不导出struct或interface实现 - 用
go:generate从proto生成stub,强制走序列化——哪怕实际走内存拷贝,也能保证未来gRPC切换零代码修改 - 错误必须封装:
payment.ErrInsufficientBalance不能是errors.New("balance too low"),而应是带Code()方法的自定义类型,方便网关统一映射HTTP状态码
最难的不是写代码,是判断某个pkg/utils该放进哪个module——它可能被三个服务共用,但其中两个服务下周就要拆出去。这时候别急着抽象,先复制三份,在各自module里独立演进,等三个月后发现80%代码雷同,再抽成独立module并迁移。过早复用比重复代码更致命。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











