
grpc-go 代码生成器版本兼容性测试解析 testdata/grpc_testing_not_regenerated 中禁止重新生成的 protobuf 代码【免费下载链接】grpc-goThe Go language implementation of gRPC. HTTP/2 based RPC项目地址: https://gitcode.com/GitHub_Trending/gr/grpc-gogrpc-go 仓库的 testdata/grpc_testing_not_regenerated 目录存放了三组刻意保留历史痕迹的 protobuf 生成代码它们分别由旧版代码生成器、新版 protoc 与中间过渡版本生成用于在测试中验证 gRPC Go 运行时对不同代际生成代码的向后兼容性。本文围绕该目录下的 README.md 展开结合源码逐文件讲解grpc.SupportPackageIsVersion编译期断言机制、protobuf 描述符字节的导出用法、MessageV1/MessageV2 API 差异以及 reflection 测试如何消费这些老代码帮助读者理解 grpc-go 如何守住生成代码兼容这条底线。目录定位为什么需要一批不重新生成的文件该目录下共有 6 个文件其中 3 个.proto源文件与 3 个由它们生成的.go文件一一对应proto 源文件生成代码生成器版本/特征用途testv3.prototestv3.go旧版 codegen声明支持grpc.SupportPackageIsVersion3验证 reflection 在旧版 gRPC API 下的行为dynamic.protodynamic.go较新的 protoc人工裁切为纯描述符字节验证对动态构造的 protobuf 消息做 reflectionsimple.protosimple_message_v1.goprotoc-gen-go v1.3.5不支持 MessageV2 API验证旧 MessageV1 API 消息的可用性README.md反复强调DO NOT REGENERATE!禁止重新生成。原因在于一旦用当前版本的代码生成器重新生成这些文件它们所代表的历史代际特征就会消失测试也就失去了验证向后兼容性的意义。这是理解整个目录的钥匙——这些文件存在的目的就是保持过时。testv3.go用旧版 gRPC API 验证 SupportPackageIsVersion3 兼容性生成代码与运行时的版本握手SupportPackageIsVersiongRPC Go 的生成代码与运行时之间通过一组编译期常量完成版本握手。在 rpc_util.go 中定义// The SupportPackageIsVersion variables are referenced from generated protocol // buffer files to ensure compatibility with the gRPC version used at compile time. const ( SupportPackageIsVersion3 true SupportPackageIsVersion4 true ... SupportPackageIsVersion9 true )生成代码会写下形如const _ grpc.SupportPackageIsVersion3的断言行。由于这行代码在编译期引用运行时包中的常量一旦生成代码所依赖的 API 代际与当前 grpc 运行时不一致编译会直接失败从而在最早的阶段暴露不兼容而不是等到运行期才报错。testv3.go 正是这条机制的活化石// This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ grpc.SupportPackageIsVersion3旧版 gRPC 调用 API 的代码特征除了版本断言testv3.go还保留了旧版 gRPC 客户端调用 API。现代生成代码通过grpc.ClientConnInterface.Invoke/NewStream调用而这份旧代码使用的是当年面向具体调用的封装函数func (c *searchServiceV3Client) Search(ctx context.Context, in *SearchRequestV3, opts ...grpc.CallOption) (*SearchResponseV3, error) { out : new(SearchResponseV3) err : grpc.Invoke(ctx, /grpc.testingv3.SearchServiceV3/Search, in, out, c.cc, opts...) ... } func (c *searchServiceV3Client) StreamingSearch(ctx context.Context, opts ...grpc.CallOption) (SearchServiceV3_StreamingSearchClient, error) { stream, err : grpc.NewClientStream(ctx, _SearchServiceV3_serviceDesc.Streams[0], c.cc, /grpc.testingv3.SearchServiceV3/StreamingSearch, opts...) ... }可以看到grpc.Invoke与grpc.NewClientStream均以包级函数形式出现这是 gRPC 早期生成代码的典型形态SearchServiceV3服务同时包含一元 RPCSearch与双向流 RPCStreamingSearch其ServiceDesc中对应声明了Methods与Streams两类描述。手工替换 context 导入README.md特别注明testv3.go生成后被手工编辑将golang.org/x/net/context替换为context。这与 testv3.go 第 37 行context context的导入相印证历史上 gRPC 曾依赖golang.org/x/net/contextcontext包进入标准库后这份旧代码通过最小改动继续参与编译而不是整体重新生成。这一细节也说明保留旧代码的价值在于测试其形态必要的小修小补如标准库迁移是被允许的。reflection 测试如何消费 testv3.gotestv3 代码的核心消费者是 reflection 服务的端到端测试 reflection/test/serverreflection_test.go。该测试将 testv3 包别名导入为pbv3并注册其服务实现func (s *serverV3) Search(context.Context, *pbv3.SearchRequestV3) (*pbv3.SearchResponseV3, error) { return pbv3.SearchResponseV3{}, nil } pbv3.RegisterSearchServiceV3Server(s, serverV3{})随后在期望结果表中用fdTestv3Byte由loadFileDesc(testv3.proto)获取的文件描述符序列化字节校验 reflection 查询应返回的符号例如服务、方法、消息、枚举与枚举值{grpc.testingv3.SearchServiceV3, fdTestv3Byte}, {grpc.testingv3.SearchServiceV3.Search, fdTestv3Byte}, {grpc.testingv3.SearchResponseV3.Result.Value.val, fdTestv3Byte}, {grpc.testingv3.SearchResponseV3.FRESH, fdTestv3Byte},这意味着即使某个服务由旧代际代码生成器产出的代码注册grpc-go 的 reflection 实现仍必须能正确返回其完整描述符。这正是该文件存在的测试价值。dynamic.go裁切到只剩描述符字节的最小动态包dynamic.proto 定义了一个仅含空消息的服务message DynamicRes {} message DynamicReq {} service DynamicService { rpc DynamicMessage1(DynamicReq) returns (DynamicRes); }而 dynamic.go 由较新的 protoc 生成后被手工编辑删除了除描述符字节以外的全部内容并将变量重命名导出为FileDynamicProtoRawDesc// FileDynamicProtoRawDesc is the descriptor for dynamic.proto, see README.md. var FileDynamicProtoRawDesc []byte{ 0x0a, 0x0d, 0x64, 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, ... }这段字节就是 gzipped 的FileDescriptorProto其中编码了包名grpc.testing、两个空消息与DynamicService服务定义。之所以保留成只有字节、没有类型是为了在测试中模拟运行时才构造出来的动态 protobuf 消息——即没有预编译的 Go 类型、只能从描述符反推结构的那类消息。reflection 测试通过loadFileDescDynamic消费这段字节serverreflection_test.go先用proto.Unmarshal还原为descriptorpb.FileDescriptorProto再用protodesc.NewFile构建protoreflect.FileDescriptor注册进全局文件注册表并以dynamicpb.NewMessageType注册动态消息类型。随后期望结果表用fdDynamicByte校验 reflection 能正确列出动态注册的符号{grpc.testing.DynamicService, fdDynamicByte}, {grpc.testing.DynamicReq, fdDynamicByte}, {grpc.testing.DynamicRes, fdDynamicByte},由此可见dynamic.go是 reflection 对非静态编译、运行时动态注册的 protobuf 类型支持情况的测试载体。simple_message_v1.go只实现 MessageV1 API 的过渡代生成代码第三组文件simple.proto与simple_message_v1.go关注的是protobuf 运行时 API 的代际切换。simple.proto只定义了一个持有字符串字段的消息message SimpleMessage { string data 1; }而 simple_message_v1.go 由protoc-gen-go v1.3.5生成。README 指出该版本doesnt support the MessageV2 API不支持 MessageV2 API因此生成代码只实现了旧的 MessageV1 接口。从代码中可以清晰看到 MessageV1 时代的标志性特征type SimpleMessage struct { Data string protobuf:bytes,1,opt,namedata,proto3 json:data,omitempty XXX_NoUnkeyedLiteral struct{} json:- XXX_unrecognized []byte json:- XXX_sizecache int32 json:- } func (m *SimpleMessage) XXX_Unmarshal(b []byte) error { ... } func (m *SimpleMessage) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { ... } func (m *SimpleMessage) XXX_Merge(src proto.Message) { ... } func (m *SimpleMessage) XXX_Size() int { ... } func (m *SimpleMessage) XXX_DiscardUnknown() { ... }XXX_前缀的Unmarshal/Marshal/Merge/Size/DiscardUnknown方法与XXX_NoUnkeyedLiteral/XXX_unrecognized/XXX_sizecache隐藏字段是github.com/golang/protobuf/proto旧式消息接口MessageV1的典型结构而新一代MessageV2消息则改为通过protoimpl与protoreflect实现。这份文件因此被保留用于验证 grpc-go 代码库对只实现了旧接口的消息类型依然能够正确处理。从仓库引用关系看该包类型还被status包的外部测试引用见 status_ext_test.go继续承担着回归验证职责。兼容性回归测试的整体设计思路将三组文件放在一起可以归纳出 grpc-go 保留它们的完整逻辑覆盖三代代码形态旧版 gRPC 生成 API SupportPackageIsVersion3断言testv3、新版 protoc 但纯描述符字节dynamic、过渡期 MessageV1 消息simple_message_v1基本覆盖了 grpc-go 历史上会遇到的生成代码代际。编译期与运行期双重验证SupportPackageIsVersion常量在编译期把关 API 兼容reflection 测试则在运行期验证旧代际注册的服务与动态构造的消息都能被正确反射。拒绝重新生成以保留真实性一旦重新生成旧代码会被新工具链改写兼容性问题将被掩盖回归测试也随之失效。因此 README.md 的三条说明实质上是该目录的维护契约与目录名not_regenerated相互呼应。小结testdata/grpc_testing_not_regenerated是一个以小见大的目录三份看似过时的生成代码分别守护着 grpc-go 在生成代码版本断言rpc_util.go 的SupportPackageIsVersion系列常量、reflection 对旧式与动态注册服务的支持serverreflection_test.go、以及旧 MessageV1 API 消息兼容性三个维度上的底线。理解这份 README 与配套源码也就理解了 gRPC Go 生态中生成代码与运行时版本强绑定这一设计哲学以及开源项目如何用刻意保留的历史文物来保证长期兼容。【免费下载链接】grpc-goThe Go language implementation of gRPC. HTTP/2 based RPC项目地址: https://gitcode.com/GitHub_Trending/gr/grpc-go创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考