微服务拆分应以解决协作卡点、发布风险与技术栈收敛为目标,按限界上下文而非模块名划分边界,服务间调用需封装client接口并交由di管理,grpc契约须严守聚合根原则,数据库共实例但schema隔离,核心在于明确数据主权归属。

拆微服务不是为了“看起来像微服务”,而是为了解决单体里团队协作卡点、发布风险高、技术栈无法收敛这些真实问题。如果当前单体跑得稳、改得快、扩得灵,别急着拆。
按限界上下文拆,而不是按模块名或表名拆
很多团队一上来就建 user-service、order-service,结果半年后发现登录要查用户基本信息+实名状态+风控等级+消息推送开关——四个字段来自四个不同业务规则,全堆在一个服务里,接口越加越多,数据库 schema 被迫开放。
真正该做的,是画出限界上下文(Bounded Context):
- 谁对“用户”这个概念有数据主权?是注册流程决定 ID 生成规则,还是会员系统决定等级升降逻辑?
- 一个需求变更是否只改一个服务?比如“增加手机号一键登录”要不要同时动
auth-service、user-service、sms-gateway?如果要,边界就错了 - 优惠券在发券、核销、风控中语义完全不同,强行塞进一个
coupon-service,必然妥协接口、共享数据库、绕过领域规则
Go 里 HTTP 服务拆分后,别在 handler 层硬编码其他服务地址
常见错误现象:http.HandleFunc("/pay", func(w http.ResponseWriter, r *http.Request) { resp, _ := http.Get("http://order-svc:8080/v1/order/123") })——这种写法会让服务失去弹性,无法做重试、超时、熔断,且本地开发时根本没法联调。
实操建议:
- 所有下游调用必须封装成
client接口,例如type OrderClient interface { GetOrder(ctx context.Context, id string) (*Order, error) },实现里才用http.Client或 gRPC stub -
client初始化交给 DI 容器(如dig或手写构造函数),不要在 handler 里new;这样测试时可轻松注入 mock 实现 - 生产环境用服务发现(如
Consul+go-micro/registry/consul)或 DNS SRV 记录,避免把order-svc:8080写死在代码或config.json里
gRPC 接口定义不写清楚,上线第一周就得返工
用 protoc 生成代码很爽,但如果你的 .proto 文件里写着 rpc GetUserWithOrders(GetUserRequest) returns (GetUserResponse),等于把聚合逻辑强绑在服务契约上。下游一要加收货地址字段,你就得改 .proto、发版、等所有客户端升级——这不是微服务,是 RPC 单体。
关键约束:
- 每个服务只暴露自己聚合根内的数据,禁止跨域组装(如
GetUserWithOrders、GetOrderWithItemsAndUser) - 字段增删需兼容旧版本:新增字段设为 optional,删除字段保留字段号但加
deprecated = true - proto 不应放在独立
api/仓库统一管理;各服务只维护自己对外契约,必要时通过import public复用基础类型
初期可共用 PostgreSQL 实例,但必须隔离 schema
初拆阶段为降低运维成本,可以复用同一套 PostgreSQL 实例,但必须满足:
- 每个服务使用独立
schema(如user_service、order_service),禁止跨schemaJOIN - 连接池初始化时指定
search_path,避免误查其他服务表 - CI/CD 流水线中,每个服务的 Flyway/Liquibase 迁移脚本只操作自己的
schema,且带前缀校验 - 一旦出现跨 schema 查询需求,说明边界已模糊,必须回退重构,而不是加白名单权限
真正的难点不在代码怎么写,而在每次需求评审时,能否坚定地问一句:“这个字段,到底该由谁负责写入和解释?”——这句话问多了,边界自然就清晰了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











