应先画限界上下文再拆分服务,避免按实体建目录导致分布式单体;核心流程中强依赖、共享状态、需事务一致的操作属同一上下文;接口契约须克制,数据主权要清晰,依赖隔离与真实服务发现是落地关键。

拆分前先画出限界上下文,别急着建 service 目录
业务逻辑复杂时,最危险的动作是直接按“用户”“订单”“支付”建三个 cmd 目录然后开干。这大概率会产出一个“分布式单体”——改个优惠券核销逻辑,要同步发版 user-service、order-service、coupon-service 三个服务。
真正该做的,是逆向推导核心流程里哪些环节必须原子化协作。比如“下单”这个动作,如果包含 CalculatePrice() → CheckInventory() → CreateOrder() 这条强依赖链,且它们共享状态(如库存锁、价格缓存)、要求事务一致性,那它们就属于同一个限界上下文,不该拆。
- 问自己:一个需求变更,是否只影响一个服务?如果改登录流程要动
user-service、auth-service、notification-service,边界就错了 - 检查高频耦合调用:如果
user.Service直接调用order.GetByID,说明没抽象接口,领域边界模糊 - 数据主权不清晰的服务,比如多个服务都往
users表写字段,迟早被 DBA 叫停
proto 接口定义必须克制,禁止跨上下文聚合
很多团队在 user.proto 里写 rpc GetUserWithOrders(GetUserRequest) returns (GetUserResponse),以为方便,结果下游一要加收货地址就得全链路发版。这不是微服务,是 RPC 单体。
接口契约只暴露本上下文能且只应提供的能力。用户服务只返回 User,订单归属由调用方自己查 order-service;优惠券发放和核销语义不同,就该拆成 coupon-issuance 和 coupon-redemption 两个 proto 包,连 schema 都不一致。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 新增字段必须设默认零值:
int32 version = 4 [json_name = "version"];,旧 client 不传也不 panic - 废弃字段用
reserved 3;标记,并在注释写明// deprecated since 2026-01, remove in v2.1 - 版本写进包名:
package userpbv2;,别指望/api/v2/user路径能约束 gRPC
初始化依赖必须隔离,go list 是你的第一道检查线
一个模块能不能拆成独立服务,不看名字,看它启动时有没有干净的生命周期。跑一遍 go list -f '{{.Deps}}' ./cmd/main,如果输出里出现 order.Repository 或共享同一个 *sql.DB 实例,那它现在只是模块,不是服务。
能拆的前提是:它有自己的 DB/Redis 初始化与关闭逻辑、不共用连接池、配置项可独立覆盖。否则你只是把单体代码物理挪到不同目录,运行时照样互相拖垮。
- 禁止在
order-service里import user-service/internal/repository或拼 SQL 查 user 表 - 下游调用必须封装成 client 接口,例如
type OrderClient interface { GetOrder(ctx context.Context, id string) (*Order, error) } - client 实现里才用
grpc.Dial或http.Client,初始化交给 DI 容器或构造函数链,绝不在 handler 里 new
本地开发必须模拟真实服务发现,别硬编码地址
http.Get("http://order-svc:8080/v1/order/123") 这种写法,在本地根本跑不通,上线后也扛不住节点故障。硬编码地址导致无法做超时控制、重试策略、熔断降级,链路追踪 tag 也串不起来。
开发环境用 localhost + 端口映射(如 order-svc 映射到 localhost:8081),生产走 Consul 或 etcd;所有 client 初始化都通过 resolver.Builder 插件注入,而不是写死字符串。
- 超时控制必须设在 client 端:
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)是唯一可靠手段 - HTTP 的
http.Client.Timeout在长连接复用下可能失效,gRPC 的context.WithTimeout才真正生效 - 健康检查不能只 ping
/health返回 200,得检查 DB 连接、Redis 连通性等真实依赖
真正卡住落地的,从来不是语法或框架选型,而是初始化依赖是否干净、proto 契约是否克制、服务发现是否真实可用。这三处不厘清,拆得再漂亮,上线后也是运维噩梦。

















