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

Go 里 Clean Architecture 落地到网络协议层(如 HTTP、gRPC、WebSocket),关键不是“加一层 adapter”,而是让协议细节彻底不出现在 domain 和 usecase 中——否则协议变更(比如从 HTTP 换成 gRPC)就会牵动业务逻辑。
protocol 层该放在哪一层?
协议处理代码(路由注册、中间件、request binding、response writing)必须属于 adapters 或 interfaces 层,不能进 usecase,更不能进 domain。常见错误是把 http.HandlerFunc 直接写在 usecase 包里,或让 usecase 接收 *http.Request。
- 正确做法:在
adapters/http下写 controller,它只做三件事:解析请求 → 调用 usecase → 格式化响应 - controller 构造函数接收 usecase 接口(如
CreateUserUsecase),不 import 任何 infra 包 - HTTP-specific 错误码(如
http.StatusConflict)只出现在 controller 层,usecase 返回的错误必须是 domain 定义的错误类型(如ErrEmailAlreadyExists) - gRPC 的
.proto生成代码建议放在adapters/grpc/pb,服务实现(UnimplementedUserServiceServer)放在adapters/grpc,同样只调用 usecase
如何避免 protocol 层污染 domain 实体?
最常踩的坑是把协议相关字段塞进 domain struct,比如加 CreatedAt time.Time 或 UpdatedAt *time.Time,或者用 json:"id" tag 绑定 HTTP 序列化逻辑。
- domain 实体字段只能用基础类型(
string,int64)、自定义枚举、或 domain 内定义的值对象(如type Email string) - 时间字段应抽象为接口传入:定义
Clock接口,在 controller 层 new 一个realClock{}注入 usecase,再注入 domain 实体方法 - JSON tag 必须剥离:domain struct 不写任何
json:、protobuf:或db:tag;DTO(如user_http_response)才负责序列化适配 - DTO 和 domain 实体之间转换必须显式、单向:controller 层用
mapToDomain()和mapToResponse(),不复用 struct,不嵌套指针引用
WebSocket / MQTT 等长连接协议怎么适配?
这类协议本质仍是输入/输出适配器,但生命周期和调用模型不同——不能按 HTTP 那样“一次请求一次 usecase 调用”。核心原则不变:协议状态管理归 adapter,业务动作仍走 usecase。
- 在
adapters/ws中启动连接管理器(ConnectionManager),它持有UserSession状态,但不碰业务规则 - 收到消息后,提取 payload → 构造 input DTO → 调用对应 usecase(如
wsUC.HandleChatMessage(input)) - usecase 执行完返回 domain event(如
MessageSentEvent),adapter 再决定推送给谁、是否广播、是否存档 - 切忌在 usecase 里调用
conn.WriteMessage()或持有*websocket.Conn—— 这等于把 transport 细节拖进业务层
协议层最容易被忽略的复杂点,是错误传播路径。HTTP 返回 400 是因为参数校验失败,gRPC 返回 InvalidArgument 是同一语义,但它们不该由 usecase 判断;应该由 adapter 做初步校验(如 JSON 解析失败、必填字段缺失),再把 clean input 交给 usecase,让 usecase 只专注领域规则校验(如“余额不足”)。混淆这两类错误,会直接破坏 Clean Architecture 的边界感。











