Go中协议层DDD落地需坚守三原则:协议语义不进domain、状态机由聚合根封装、序列化严格隔离在transport层;解析器放transport/,domain类型禁用Marshal/Unmarshal,事件只含业务字段。

Go 里没有“网络协议层的 DDD 框架”,也不存在 protocol.NewDomainEvent() 这种东西。DDD 在协议层(比如 TCP/UDP 报文解析、gRPC 流控、MQTT 会话状态机)落地,核心不是加抽象层,而是用 Go 原生机制守住三件事:协议语义不进 domain、状态机边界由聚合根封装、序列化/反序列化严格隔离在 transport 层。
协议解析逻辑不能塞进 domain 包
常见错误是把 DecodeMQTTPacket() 或 ParseHTTP2Frame() 放到 domain/ 下,甚至让 User 结构体直接实现 UnmarshalBinary()。这会导致 domain 包 import encoding/binary、github.com/quic-go/quic-go 等 infra 依赖,分层瞬间崩塌。
正确做法是:
- 协议解析器(如
mqtt.PacketDecoder)放在transport/mqtt/或internal/codec/,只输出干净的领域对象(如*domain.SessionStartEvent) -
domain/包内所有类型禁止定义MarshalJSON()、UnmarshalBinary()等方法 - 若需校验报文字段(如 MQTT 的 QoS 合法性),校验逻辑写在 transport 层,错误转为
domain.ErrInvalidProtocolState这类领域错误,而非返回io.ErrUnexpectedEOF
连接/会话状态必须用聚合根建模
像 MQTT ClientID 绑定、WebSocket 连接生命周期、QUIC stream 复用这些,本质是带唯一标识 + 不变量 + 状态流转的聚合。别用全局 map + mutex 模拟,也别把状态字段暴露成 public。
实操要点:
- 聚合根结构体(如
*domain.ClientSession)定义在domain/,ID 字段公开(ID string),内部状态小写(state sessionState、streams map[uint64]*stream) - 构造函数(
NewClientSession(id string))做基础校验:if !validClientID(id) { return nil, errors.New("invalid client id") } - 所有状态变更走方法:
func (s *ClientSession) HandlePingResp() error,返回error而非 bool;禁止外部直接改s.state = Connected - 跨协议引用只存 ID:
sessionID string,不存*websocket.Conn或quic.Stream指针
序列化与传输细节必须和领域事件解耦
很多人一写事件就下意识用 json.Marshal(event) 或发到 chan []byte,结果 domain 包被迫 import encoding/json,或者 event 类型里混入 protobuf.Message 接口。
关键约束:
- 领域事件类型(如
ClientDisconnected)只含业务字段(ClientID、Reason),不带任何 tag(无json:、protobuf:) - 事件发布前,由
transport/codec层负责转换:codec.EncodeMQTTEvent(e)返回[]byte,codec.EncodeGRPCEvent(e)返回*pb.Event - eventbus.Publish() 参数类型是
interface{}或自定义pubsub.Event,infra 层实现不 import domain 的具体 event 类型 - 测试时用
codec.NoopEncoder替换真实编码器,确保 domain 层逻辑可离线验证
最易被忽略的是:协议层的“领域”不是 TCP/IP 模型里的层,而是业务语义层——比如“一个 MQTT 客户端从断连到重连成功”才是领域事件,而“收到 PINGREQ 报文”只是 transport 层信号。混淆这两者,DDD 就退化成了带包名的 if-else。











