clean architecture 分层切包是解决 go 微服务结构失焦的关键:entity 仅含纯数据结构,usecase 依赖抽象接口不碰具体实现,repo/controller 负责对接外部系统;错误处理需分层裁剪上下文;初始化逻辑须集中于 main 函数。

Go微服务项目一旦上线迭代几次,就容易出现 handler 直接调用 repo、usecase 里塞 HTTP 客户端、错误处理各写各的——这不是代码量问题,是结构失焦。重构不是重写,而是把边界重新焊牢。
按 Clean Architecture 分层切包,别让 internal 目录变成垃圾场
很多团队把 internal 当成“还没想好放哪”的默认收纳箱,结果里面混着数据库操作、HTTP handler、领域逻辑,改一个字段要 grep 十个文件。真正的分层不是靠目录名,而是靠依赖方向:
-
internal/entity只含纯数据结构,不 import 任何外部包(包括database/sql或net/http) -
internal/usecase只依赖entity和抽象接口(如repo.UserRepo),绝不碰具体实现 -
internal/repo和internal/controller才负责对接 PostgreSQL、gRPC 或 Gin —— 它们实现接口,但不被上层直接 import
常见错误:在 usecase 里 new 一个 sql.DB 或调用 http.Post。这会让业务逻辑和传输细节耦死,单元测试只能跑 mocks 堆成山。
用接口隔离第三方依赖,而不是封装一层“工具函数”
看到 utils.HttpDo() 或 helpers.DBQuery() 这类包,基本就是可维护性开始滑坡的信号。它们看似简化了调用,实则把协议细节(超时、重试、错误码映射)和业务逻辑焊在一起。
- 正确做法:定义
type PaymentClient interface { Charge(ctx context.Context, req ChargeReq) (ChargeResp, error) } - 让具体实现(比如 StripeClient)去处理证书、签名、429 重试逻辑
- usecase 只管“我要扣款”,不关心“怎么扣”——这样换支付渠道时,只需替换实现,不用动业务流
容易踩的坑:接口方法设计过宽(比如 Do(ctx, url, method, body)),导致所有调用都走同一个入口,丧失语义和可测性。
错误处理必须带上下文,但别堆砌 fmt.Errorf 链
Go 的 error 链(%w)不是用来拼凑日志的。你看到 fmt.Errorf("failed to create order: %w", err) 在五层调用栈里重复出现,说明错误传播没做裁剪。
- 底层(如 repo 层)返回原始 error,或包装成领域错误(
ErrOrderNotFound),不加业务上下文 - usecase 层才注入关键上下文:
fmt.Errorf("order creation failed for user %s: %w", userID, err) - controller 层转成 HTTP 状态码,但不再用
%w向外抛——终端用户不需要知道 DB 连接失败还是 Redis 超时
性能影响:过度包装 error 链会拖慢 panic 恢复路径,尤其在高频接口里。真要 debug,靠 trace_id 关联日志比层层 unwrap 更可靠。
配置和初始化逻辑必须收口,禁止 init() 或全局变量
启动时从 etcd 加载配置、连接数据库、注册 gRPC server —— 这些动作如果散落在各个包的 init() 函数里,会导致:
- 测试时无法控制依赖启动顺序
- 某些配置项只在 prod 环境生效,dev 下 silent fail
- CI 构建时因网络不可达直接 panic,而非优雅退出
实操建议:所有初始化逻辑集中到 cmd/xxx/main.go 的 main() 函数里,按明确顺序执行,并检查每一步的 error。配置结构体用 struct tag 标记 required 字段,启动时用 validator 包校验,而不是等第一次 DB 查询才报错。
最常被忽略的一点:分层不是目的,而是为了能独立测试 usecase。如果你的 usecase 包还需要起一个 PostgreSQL 实例才能跑测试,那分层已经失效了。











