不能将gRPC和HTTP逻辑混在同一handler里,因业务逻辑与传输协议必须解耦;混写会导致新增协议(如WebSocket/MQTT)时需重写路由、错误处理及上下文注入;正确做法是通过adapter将请求统一转为service层的纯Go调用,service不感知协议细节。

协议适配器模式:为什么不能把 gRPC 和 HTTP 逻辑混在同一个 handler 里
因为业务逻辑和传输协议必须解耦。一旦你在 http.HandlerFunc 里直接调用 grpc.Invoke,或在 pb.UnimplementedServiceServer 里写 http.Error,后续加 WebSocket 或 MQTT 就得重写全部路由、错误处理和上下文注入逻辑。
正确做法是让 service 层只认 RequestContext 和纯 Go struct:
- 所有 adapter 把请求“翻译”成 CreateUser(ctx, req) 调用
- service 不知道自己被 HTTP 还是 gRPC 调用,也不关心返回值要不要序列化成 JSON
- 新增协议只需新增一个 adapter 包,不改 service 代码
如何用 net/http.Handler 统一入口但分流多协议
http.ServeMux 或 chi.Router 只负责路径匹配,真正的协议识别靠请求头和 body 特征:
- 检查
r.Header.Get("Upgrade") == "websocket",是则交给gorilla/websocket处理 - 检查
r.Header.Get("Content-Type") == "application/grpc"或路径以/grpc/开头,走 gRPC 帧解析分支 - 对普通
application/json请求,走标准 REST 解析 - 不要依赖 magic bytes 自动识别协议——TLS 加密后无法读取 payload 开头,且 Protobuf/FlatBuffers/自定义二进制格式的前缀可能冲突
gRPC 到 HTTP 的转换必须显式处理帧头和压缩标志
gRPC over HTTP/2 的帧格式不是简单 POST body,而是带 5 字节前缀的二进制流:
- 第 1 字节是压缩标志(0x00 表示未压缩)
- 后 4 字节是大端序 uint32,表示后续 Protobuf 消息长度
- 网关若直接把 JSON body 当作 gRPC payload 发给后端,会触发 rpc error: code = Internal desc = stream terminated by RST_STREAM
手写转换时必须:
- 用 binary.Write 写入帧头
- 用 proto.Marshal 编码请求体
- 设置 content-type: application/grpc 和 te: trailers
- 对响应同样做反向解帧和 JSON 转换
错误映射不能靠 status code 数字硬编码
同一个业务错误,在不同协议下表达方式完全不同:
- HTTP 需要 http.StatusConflict(409)
- gRPC 要 codes.AlreadyExists
- WebSocket 可能发 {"error": "user_exists", "code": 409}
所以定义统一的 AppError 类型,包含:
- Code string(业务码,如 "USER_EXISTS")
- HTTPStatus int
- GRPCCode codes.Code
- adapter 层按协议选择字段映射,service 层永远只返回 AppError
容易忽略的是:gRPC 流式响应的错误必须在 trailer 中传递,不能塞在消息体里;而 HTTP/1.1 没有 trailer 概念,得降级到 body + status code。


















