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