协议层代码必须置于adapters或interfaces层;HTTP/gRPC/WebSocket处理逻辑(路由、绑定、响应)不得进入usecase或domain,DTO与domain实体须显式单向转换,时间字段用Clock接口抽象,JSON/protobuf/db tag严禁出现在domain中。

协议层代码必须放在 adapters 或 interfaces 层,否则 Clean Architecture 就失效了。 一旦 HTTP、gRPC 或 WebSocket 处理逻辑渗入 usecase 或 domain,协议变更(比如从 HTTP 切到 gRPC)就会直接改业务逻辑,这不是解耦,是套娃。
HTTP handler 为什么不能写在 usecase 包里
常见错误现象:usecase/user.go 里出现 func CreateUserHandler(w http.ResponseWriter, r *http.Request),或者 usecase.RegisterUser(*http.Request) 这类签名。
- 违反依赖规则:usecase 层本该只依赖 domain 接口,现在却强绑
net/http - 测试成本飙升:想测注册逻辑,就得 mock
*http.Request和http.ResponseWriter,还得处理 header、body 解析等协议细节 - 复用无从谈起:CLI 工具或定时任务调用同一注册逻辑时,得重写一遍绑定和错误映射
- 正确位置是
adapters/http/user_controller.go,它只做三件事:解析请求 → 调CreateUserUsecase→ 写响应
DTO 与 domain 实体之间必须显式单向转换
常见错误现象:直接把 domain.User 暴露给 HTTP 层,结构体里带 json:"id"、bson:"_id" 或 CreatedAt time.Time 字段;或者用 sql.NullString 塞进 domain struct。
- domain 实体字段只能是基础类型(
string、int64)、自定义枚举或 domain 内定义的值对象(如type Email string) - 时间字段必须抽象为接口传入:
Clock接口由 controller 构造并注入 usecase,domain 方法内调用clock.Now(),而非直接用time.Now() - JSON/protobuf/db tag 严禁出现在 domain 中;DTO(如
UserHTTPResponse)才负责序列化适配 - 转换函数命名要明确:
userToHTTPResponse()和httpRequestToUserDomain(),不复用 struct,不嵌套指针引用
gRPC 和 WebSocket 的适配要点
gRPC 的 .proto 生成代码必须放在 adapters/grpc/pb,服务实现(UnimplementedUserServiceServer)放在 adapters/grpc,且只调用 usecase 接口。
- WebSocket 不是“一次连接一次 usecase”,而是长连接状态管理归 adapter:在
adapters/ws中启动ConnectionManager,它持有*User(注意不是 domain.User,而是 adapter 自维护的连接上下文) - 业务动作仍走 usecase:比如用户发消息,adapter 解析 payload → 调
SendMessageUsecase→ 把结果推回对应 connection - 错误码隔离:HTTP 返回
http.StatusConflict,gRPC 返回codes.AlreadyExists,这些只出现在 adapter 层;usecase 返回的必须是 domain 错误类型,如ErrEmailAlreadyExists
最容易被忽略的是时间字段和空值语义——CreatedAt string 看似简单,但若没用 Clock 接口统一注入,测试时就无法控制时间点;*string 和 sql.NullString 表面都表示可空,但后者会让内存仓库 mock 失败,因为它的 Scan 方法依赖 database/sql。这些不是风格选择,是架构能否真正解耦的分水岭。


















