golang微服务演进必须分三步:先单体跑通业务,再统一internal/shared基础能力(配置、日志、错误、db),最后按限界上下文物理拆分;跳过第二步将导致伪微服务——配置散落、日志不可查、错误难追踪、连接池混乱。

Go微服务不是“一拆了之”,而是分三步走:先稳住单体、再统一 shared 基础设施、最后按限界上下文物理隔离。跳过第二步,90% 的团队会陷入“伪微服务”——代码拆了,但配置不统一、日志不可查、错误难追踪、DB 连接池各自为政。
为什么必须先重构 internal/shared?
很多团队在 cmd/ 下建了 user-service 和 order-service,却没动 internal/shared,结果每个服务都重复实现:
-
loadConfigFromYAML()各写一遍,Consul fallback 逻辑不一致,配置变更无法通知 -
logger.With().Str("service", "xxx")手拼字段,trace_id 缺失,链路断层 -
errors.NewBadRequest()没封装 HTTP 状态码和错误前缀,下游无法结构化解析 -
gorm.Open()直接调用,连接池参数(SetMaxOpenConns、SetConnMaxLifetime)五花八门,DB 被打爆
shared 模块不是“工具包”,它是所有服务共用的运行时契约。它得提供:config.Loader(支持热重载)、logger.SugaredLogger(预埋 trace_id)、errors.Error(含 code、status、cause)、db.DBTX(统一封装连接池 + 自动迁移开关)。
拆分前怎么判断两个模块该不该分开?
别按“用户”“订单”这种名词切,看三条硬指标:
-
数据归属是否独立:如果
order和order_item共用一张表或强事务依赖,它们属于同一限界上下文,不能拆;user和order各自有主库、通过事件同步,才具备拆分基础 - 变更频率是否差异大:促销规则每天改,用户认证半年不动——这两块放一起发布,只会互相阻塞
-
跨包调用是否已解耦:检查
user.Service是否直接依赖order.Repository;若存在,先定义order.OrderReader接口,让 user 只依赖抽象,再考虑进程隔离
没满足这三条就硬拆,等于把单体问题复制成 N 份。
拆完之后 HTTP 调用怎么写才不踩坑?
常见错误是 handler 里直接 http.Get("http://order-svc:8080/v1/order/123"),后果是:
- 超时、重试、熔断全靠裸写,不可维护
- 本地开发时无法 mock,联调全靠起全套服务
- 服务地址写死,换注册中心就得改代码
正确做法是:
- 定义 client 接口:
type OrderClient interface { GetOrder(ctx context.Context, id string) (*Order, error) } - 实现类里用
http.Client或 gRPC stub,注入超时、重试策略 - client 初始化交给 DI(如
dig.Inject或构造函数),测试时可注入mockOrderClient - 生产环境通过服务发现(如
consul+go-micro/registry)解析地址,绝不写死 host:port
proto 文件也一样:别建单独的 api/ 仓库让所有服务 go get,而应按领域拆到各服务自己的 api/ 下,只共享必要 message,避免改一个字段全链路发版。
真正的难点不在“怎么拆”,而在“怎么让拆完的服务还能像单体一样可测、可观、可运维”。shared 模块的厚度,决定了后续演进是平滑还是灾难。











