微服务拆分应以业务能力与限界上下文为边界,而非技术结构;需通过事件驱动、契约优先(gRPC+proto)、接口抽象、依赖倒置和最终一致性实现服务自治。

微服务拆分不是“把单体代码按文件夹切开”,而是围绕业务能力重新定义责任边界——拆得对,系统可演进;拆错了,就是分布式单体。
按限界上下文划服务边界,而不是按表或功能点
很多团队一上来就建 user-service、order-service,结果发现所有服务都要查 users 表、都依赖 User.GetProfile(),最终变成强耦合的“RPC 调用网”。真正该问的是:谁拥有用户数据的写入主权?谁负责用户状态变更的业务规则?
- 订单创建时需要“用户是否实名”——这不是订单服务去查用户表,而是用户服务发布
UserVerifiedEvent,订单服务监听并缓存必要字段 - 如果
user-service同时被order-service和notification-service频繁同步调用,说明它暴露了不该暴露的细节(比如GetUserWithPreferences),应收缩接口,只返回id、status等核心字段 - 初期可用 PostgreSQL 的不同
schema隔离,但必须禁止跨 schema JOIN 或直接访问对方表——哪怕只是SELECT * FROM order_service.orders
用 gRPC + proto 定义契约,别用 HTTP 手搓 JSON 接口
用 http.HandlerFunc 暴露 /v1/user 看似快,但很快会遇到字段名不一致、版本混乱、无文档、客户端反序列化 panic 等问题。gRPC 不是“更高级的 HTTP”,它是契约驱动的协作机制。
- 所有跨服务调用必须通过
.proto文件生成,禁止在代码里硬写http.Post(...)去调另一个服务的 REST 接口 - 字段增删必须向后兼容:
reserved 3;标记废弃字段,并注释下线时间;新增字段用optional int32 timeout_ms = 4 [json_name = "timeout_ms"];,零值即默认行为 - 包名带版本,如
package user.v2;,而不是靠 URL 路径/api/v2/user——后者对 gRPC 无效,且混淆了传输层和语义层
服务自治的关键动作:接口抽象、依赖倒置、事件驱动
一个服务能不能独立演进,不取决于它有没有单独部署,而取决于它是否能不改代码就替换掉依赖项。Go 没有接口继承,但正适合做轻量抽象。
立即学习“go语言免费学习笔记(深入)”;
- 订单服务不该 import
payment/internal/client,而应定义自己的接口:type PaymentProcessor interface { Charge(ctx context.Context, req ChargeRequest) (string, error) } - 启动时用
wire或fx注入具体实现(如grpcPaymentProcessor),本地测试可直接注入mockPaymentProcessor - 库存扣减失败不能让订单创建同步失败——订单服务只发
OrderCreatedEvent,库存服务异步消费,允许最终一致;否则一次 DB 故障就导致整个下单链路雪崩
避免循环依赖和“伪拆分”的信号
当两个服务互相调用、或共用同一份配置/错误码/模型结构体时,边界已经失效。这不是微服务,是披着多进程外衣的单体。
- 检查
go.mod:如果order-service的require列表里有user-service v0.1.0,立刻删掉——它们之间只能通过 proto 契约通信,不能有 Go 包级依赖 - 运行
go list -f '{{.Deps}}' ./cmd/order-service | grep user,若输出非空,说明存在隐式依赖(比如共用了shared/model) - CI 流水线里加一条检查:每个服务的
internal/目录不能被其他服务的go build引用到——这是 Go 语言级的封装保障
最常被忽略的一点:拆分不是一次性工程任务,而是持续识别变化节奏的过程。当“优惠券发放”和“优惠券核销”开始由不同团队维护、上线频率相差 5 倍、失败容忍策略完全不同,这时候才真正到了该拆开的时刻——不是因为架构图好看,是因为业务已经长出了不同的骨头。










