新项目应优先选grpc而非twirp:grpc通过.proto契约、统一错误码、丰富生态和http/2优势避免后期返工;twirp仅适用于浏览器直调、老代理限制或临时poc三类场景,本质是协议降级。

新项目直接上 gRPC,除非你明确需要 HTTP/1.1、浏览器直调、或被老代理卡死——Twirp 不是“轻量替代”,而是“协议降级选择”。
gRPC 为什么在 Go 新服务中几乎不可绕过
它不是最简单的,但能最大程度避免后期返工:IDL(.proto)即契约,工具链(protoc + grpc-go)稳定,Status.Code() 统一错误处理,grpcurl 一行命令就能查服务接口,grpc-gateway 可选暴露 REST 接口,且所有主流服务发现(Consul/Nacos/Etcd)、链路追踪(OpenTelemetry)、mTLS 都原生支持。
常见踩坑点:
-
rpc error: code = Unavailable desc = transport is closing多半是 TLS 握手失败、HTTP/2 被中间件(如旧版 Nginx)截断,或客户端未设KeepAlive参数 - 滥用
google.protobuf.Any或嵌套过深的message,会导致客户端反序列化失败时只报cannot unmarshal proto: unknown field "xxx",难以定位字段来源 - 不写
service的option (google.api.http)就想用grpc-gateway暴露 HTTP 接口——会 404,必须显式声明映射
Twirp 真正适用的三个具体场景
它本质是 “Protobuf over HTTP/1.1”,生成的 handler 是标准 http.HandlerFunc,没有 gRPC 的二进制帧和状态码透传逻辑,调试路径更直,但也因此放弃了很多关键能力。
适合用 Twirp 的情况:
- 客户端是浏览器、curl 或 Postman,且无法升级到 HTTP/2(比如某些内网环境强制走 HTTP/1.1)
- 服务必须部署在 AWS ALB / 老版本 Nginx 后面,而它们对 HTTP/2 支持不完整或默认关闭
- 只是临时 POC 或内部管理后台的简单服务间调用,不需要流式、健康检查探针、或统一重试策略
注意:twirp-gen 默认生成的错误映射是硬编码的——codes.NotFound → 404、codes.Unauthenticated → 401,但无法像 gRPC 那样通过 Status.FromError(err).Code() 在 middleware 中做细粒度重试控制。
代码生成与运行时行为差异很实在
两者都用 .proto 定义服务,但生成逻辑和运行时依赖完全不同:
-
gRPC必须用protoc-gen-go-grpc,生成的ClientConn依赖grpc.Dial(),底层强绑定 HTTP/2 连接池和流控 -
Twirp用twirp-gen(Go 实现,无需额外 protoc 插件),生成的是纯http.Handler和http.Client封装,可直接塞进chi、gorilla/mux,甚至加http.Transport自定义逻辑 -
Twirp客户端默认发POST /twirp/{package}.{service}/{method},响应体是裸 Protobuf 或 JSON;gRPC客户端发的是 HTTP/2 POST + binary frame,路径固定为/package.Service/Method
性能上,gRPC 的多路复用和头部压缩在高并发小包场景下优势明显;Twirp 在单次请求延迟上可能略低(无 HTTP/2 handshake 开销),但连接复用率和吞吐上限受限于 HTTP/1.1。
别被“轻量”误导,真正代价在扩展性上
Twirp 看似省掉 protoc 插件、不用开 HTTP/2、curl 一把梭,但一旦业务变复杂,你会立刻撞墙:
- 不支持任何流式通信(server-streaming / client-streaming / bidi),想加实时日志推送?换框架
- 健康检查探针(如
/healthz)无法复用 gRPC 的grpc.health.v1.Health标准,得自己实现 - 服务发现系统(如 Consul)的 gRPC 健康检查机制不识别 Twirp 的 HTTP/1.1 handler,需额外适配
- 没有
grpc.WithBlock()、grpc.FailOnNonTempDialError()这类连接控制语义,超时和重连逻辑全得自己补
真正容易被忽略的点:Twirp 的 context.Deadline 不会自动透传到 HTTP 层,你得手动把 ctx.Deadline() 转成 http.Request.WithContext(),否则超时控制就失效了。











