connect-go v2 现已推出
今天,我们发布了 connect-go v2,这是 Connect 的 Go 实现的新主要版本。
别慌,这不是 Connect 协议的 v2 版本。破坏性变更发生在 Go API 层面,因此 v2 客户端和 v1 服务器依然可以正常通信。
此外,v1 不会退役:它将无限期地继续接收修复和安全补丁。由于 v2 使用模块路径 connectrpc.com/connect/v2,两个版本可以在同一程序中共存,你可以逐个服务迁移,或者完全不迁移。
下面我们来看看 v2 有哪些变更,以及为什么我们要引入这些破坏性变更:
- 淘汰了过于复杂的泛型用法
- 将核心库从
net/http中解耦 - 修正了拦截器接口
- 移除了一种关于错误信息的高风险行为
不再使用泛型包装器
事实证明,我们对泛型的使用带来的痛苦超过了其价值。在设计 v1 时,我们将一元请求和响应封装在两种泛型类型之一中,即 connect.Request[T] 或 connect.Response[T],其中 T 是消息类型。这使得调用元数据中的 headers、trailers 等内容可直接获取,无需深入 context.Context。我们认为这种便利性值得付出泛型类型带来的复杂度。
我们错了。经过四年的生产环境使用,我们发现大多数调用根本不需要这些包装器。
connect-go v1 要求你在调用 RPC 方法之前先调用 connect.NewRequest(...),并通过 .Msg 访问实际消息。这看似微不足道,但使得 connect-go 与 Go RPC 生态格格不入,在那里大家预期的函数签名是 func(context.Context, *Request) (*Response, error)。这导致从 gRPC-Go 迁移到 ConnectRPC 变得不必要地复杂。
我们对这一变更进行了长时间的“beta 测试”。在 v1.19.0 中,我们在生成 ConnectRPC 代码时引入了一个 simple 标志。反馈积极,许多试用过的开发者更偏好这种更简单的签名。
因此,在 v2 中,一元 RPC 具有与 gRPC-Go 相同且熟悉的形状:
-Say(context.Context, *connect.Request[SayRequest]) (*connect.Response[SayResponse], error)
+Say(context.Context, *SayRequest) (*SayResponse, error)如果你一直在观望,迟迟没有把 gRPC-Go 代码库迁移到 Connect,那么 v2 会让这次迁移轻松许多。
移除泛型还能减小二进制体积:编译器不再为每个 RPC 生成一套 client 和方法集。把 Buf CLI 迁移到 v2 后,strip 之后的二进制体积缩小了约 10%。
需要元数据时,仍然可以通过从 context 中取出的 *connect.CallInfo 获得。完整细节请参阅 v2 指南中的 Generated code 部分。
可插拔的传输层
v1 是围绕 HTTP 语义构建的,用户提供普通的 http.Client 实例,服务端则以 http.Handler 的形式实现。可想而知,这种方式对 HTTP 来说效果不错,却阻碍了 connect-go 在其他传输方式上的使用。如果想通过 WebSockets 或 stdin/stdout 使用 Connect,就得在 HTTP 语义之间做转换。
在 v2 中,HTTP 支持被移到了 connecthttp,其他传输方式可以直接复用同一套生成的 client 和 handler,无需接触 net/http。
我们还新增了 connectinprocess,一个内存传输实现。测试 Connect 端点时不再需要复杂的缓冲技巧,也不用启动 HTTP 服务器。完整细节请参阅 v2 指南中的 Transports 部分。
统一的拦截器模型
v1 的 Interceptor 接口在一些关键场景下既受限又别扭。Unary handler 拦截器在解压和解码之后才会执行。认证拦截器虽然可以拒绝请求,但服务器此时已经完成了解码和解压的开销,这让未经认证的调用方能在凭证校验之前就消耗服务器资源。想要更早拒绝请求,认证逻辑只能放到 RPC handler 外层的 HTTP middleware 中运行。
还有其他一些问题:
- Unary 拦截器的计时不含反序列化耗时,而流式拦截器的计时包含它,这会导致指标失真。
- 拦截器无法读取网络上的消息大小(#665)。
- 流式拦截器比单体拦截器更难实现,因此我们发现许多用户仅对单体调用应用拦截器逻辑。
v2 用两种函数类型替换了单一的 Interceptor 接口,即 ClientInterceptor 和 ServerInterceptor。
-type UnaryFunc func(context.Context, AnyRequest) (AnyResponse, error)
-type StreamingClientFunc func(context.Context, Spec) StreamingClientConn
-type StreamingHandlerFunc func(context.Context, StreamingHandlerConn) error
-
-type Interceptor interface {
- WrapUnary(UnaryFunc) UnaryFunc
- WrapStreamingClient(StreamingClientFunc) StreamingClientFunc
- WrapStreamingHandler(StreamingHandlerFunc) StreamingHandlerFunc
-}
+type ClientFunc func(ctx context.Context, spec Spec) (ClientStream, error)
+type ServerFunc func(ctx context.Context, spec Spec, stream ServerStream) error
+
+type ClientInterceptor func(next ClientFunc) ClientFunc
+type ServerInterceptor func(next ServerFunc) ServerFunc在 v2 中,两种拦截器类型均覆盖单体和流式调用,且服务器拦截器可在解压缩或解码之前拒绝请求。
有关编写 v2 拦截器的更多信息,请参见 v2 指南中的 拦截器 章节。
更安全的错误处理
在 v1 中,从处理器返回普通的 Go 错误会将错误信息发送给客户端,可能会暴露数据库错误中的敏感细节。在 v2 中,只有本地创建的 *connect.Error 值才会将其消息发送给客户端。
有关完整的细节,请参见 v2 指南中的 错误处理 章节。
迁移
迁移是可选的,v1 和 v2 可以在同一个模块中共存。如果你选择迁移,有一个工具可以处理大部分机械性工作:
go install connectrpc.com/connect/v2/cmd/connect-go-v2-migrate@latest迁移指南 介绍了如何安装 v2 生成器并运行迁移工具。
请注意,部分更改仍需人工介入。自定义拦截器必须重写为新的函数类型,otelconnect.NewInterceptor() 需要拆分为服务端和客户端两种形式,且 vanguard 的 v2 API 发生了较大变更。工具会在无法安全迁移代码的每个实例处打印警告,这些警告可以作为迁移过程中的待办事项清单。
如有反馈或需要迁移协助,请发送邮件至 feedback@buf.build 或 加入我们的 Slack。
下一篇文章