微服务模块化本质是按职责边界切分而非简单拆包;需以接口定义和单向依赖为核心,通过domain/app/transport分层、调用方定义接口、构造函数显式注入依赖等实践保障高内聚低耦合。

微服务模块化不是拆包,而是按职责边界切分
Go 里没有“模块化”关键字,go mod 管理的是依赖,不是业务模块。真正决定模块化质量的,是接口定义和包间依赖方向。常见错误是把所有 handler、service、dao 堆进一个 internal/xxx 包里,结果改个路由逻辑要 grep 五个文件。
正确做法是每个业务域自包含:比如用户域,就该有 user/domain(实体+领域接口)、user/app(用例逻辑)、user/transport(HTTP/gRPC 入口)——这三个包只允许从下往上依赖,domain 不能 import transport。
- 用
go list -f '{{.Deps}}' ./user/app检查是否意外引入了 transport 层 - 领域接口定义在
domain包里,实现放在infra或app,避免 service 层直接操作数据库驱动 - 跨域调用必须走 interface,禁止直接 import 另一个 domain 的 struct
接口隔离比 package 分割更重要
很多团队花时间设计目录结构,却忽略接口暴露粒度。比如 UserRepo 接口如果定义了 Create、Update、Delete、FindByEmail、FindAllWithRole,那 auth 域想校验邮箱唯一性,就必须依赖整个 repo,哪怕它只需要 FindByEmail。
解决方法是按使用方收敛接口:让 auth 域定义自己的 EmailChecker 接口,由 user/infra 实现并注入。这样 auth 不知道 user 数据库怎么存,也不用升级时跟着改。
立即学习“go语言免费学习笔记(深入)”;
- 接口定义写在调用方包里(如
auth/domain),实现写在被调方(如user/infra) - 避免
interface{}或泛型过度抽象,Go 的接口本就轻量,宁可多几个小接口,别搞一个大而全的UserService - 用
go vet -shadow检查同名变量遮蔽,尤其在嵌套 struct 实现多个接口时容易漏掉字段初始化
启动时依赖注入必须显式传递
别用全局变量或 init 函数初始化 service。常见反模式是 user.NewService() 里直接 new db 连接,导致测试难 mock,也掩盖真实依赖。微服务启动顺序敏感,db 连不上,http server 就不该起来。
标准解法是构造函数接收所有依赖,主函数里串起链条:
func main() {
db := sql.Open(...)
repo := user.NewRepo(db)
svc := user.NewService(repo)
handler := user.NewHandler(svc)
http.ListenAndServe(":8080", handler.ServeMux())
}
- 每个 NewXXX 函数参数不超过 3 个,否则拆成 config struct,但 struct 字段必须全小写(避免导出后被误用)
- 数据库连接池、redis client、logger 这类共享资源,统一在 main 初始化后传入各模块,不各自 new
- 用
go build -ldflags="-s -w"减小二进制体积,微服务镜像里不需要 debug 符号
跨服务通信别绕过 domain 层直接拼 JSON
调其他服务时,很多人直接用 http.Post + json.Marshal 发 raw payload,结果对方一加字段,这边就 panic:invalid character '}' after top-level value。
正确姿势是把远程调用封装成本地接口,和内部 service 一样使用:
type OrderClient interface {
CreateOrder(ctx context.Context, req CreateOrderReq) (CreateOrderResp, error)
}
// 在 order/transport/http/client.go 里实现
func (c *httpClient) CreateOrder(ctx context.Context, req CreateOrderReq) (CreateOrderResp, error) {
// marshal + http.Do + unmarshal
}
- client 接口定义在
order/domain,实现在order/transport/http/client,调用方只依赖 interface - 错误类型必须统一转换,比如把 HTTP 400 映射为
user.ErrInvalidEmail,而不是裸抛errors.New("bad request") - 超时、重试、熔断这些策略写在 client 实现里,domain 层完全无感知


















