:::info 日期:2020 年 03 月 02 日
作者:Joe Tsai, Damien Neil, and Herbie Ong
原文链接:https://go.dev/blog/protobuf-apiv2 :::

介绍

我们很高兴地宣布发布 Go API 的主要修订版,用于协议缓冲区,这是 Google 的语言中立数据交换格式。

一个新 API 的动机

2010 年 3 月,Rob Pike 宣布了 Go 的第一个协议缓冲区绑定。 Go 1 不会再发布两年。

自首次发布以来的十年中,该软件包随着 Go 一起成长和发展。 其用户的需求也在增长。

许多人想编写使用反射来检查协议缓冲区消息的程序。 反射包提供了 Go 类型和值的视图,但省略了协议缓冲区类型系统的信息。 例如,我们可能想要编写一个函数来遍历日志条目并清除任何注释为包含敏感数据的字段。 注释不是 Go 类型系统的一部分。

另一个常见的愿望是使用除协议缓冲区编译器生成的数据结构之外的数据结构,例如能够表示其类型在编译时未知的消息的动态消息类型。

我们还观察到一个常见的问题来源是 proto.Message 接口,它标识生成的消息类型的值,几乎没有描述这些类型的行为。 当用户创建实现该接口的类型(通常是无意中将消息嵌入另一个结构中)并将这些类型的值传递给期望生成消息值的函数时,程序会崩溃或行为不可预测。

所有这三个问题都有一个共同的原因和一个共同的解决方案:Message 接口应该完全指定消息的行为,并且对 Message 值进行操作的函数应该自由地接受任何正确实现该接口的类型。

由于在保持包 API 兼容的同时更改 Message 类型的现有定义是不可能的,我们决定是时候开始研究一个新的、不兼容的 protobuf 模块的主要版本了。

今天,我们很高兴发布这个新模块。 我们希望您能喜欢。

反射

反射是新实现的旗舰功能。 类似于 reflect 包如何提供 Go 类型和值的视图,google.golang.org/protobuf/reflect/protoreflect 包根据协议缓冲区类型系统提供值的视图。

对 protoreflect 包的完整描述对于这篇文章来说可能太长了,但让我们看看我们如何编写我们之前提到的日志清理函数。

首先,我们将编写一个 .proto 文件,定义 google.protobuf.FieldOptions 类型的扩展名,以便我们可以将字段注释为是否包含敏感信息。

  1. syntax = "proto3";
  2. import "google/protobuf/descriptor.proto";
  3. package golang.example.policy;
  4. extend google.protobuf.FieldOptions {
  5. bool non_sensitive = 50000;
  6. }
  1. 我们可以使用此选项将某些字段标记为非敏感字段。
  1. message MyMessage {
  2. string public_name = 1 [(golang.example.policy.non_sensitive) = true];
  3. }
  1. 接下来,我们将编写一个 Go 函数,它接受任意消息值并删除所有敏感字段。
  1. // Redact clears every sensitive field in pb.
  2. func Redact(pb proto.Message) {
  3. // ...
  4. }
  1. 这个函数接受一个 proto.Message,一个由所有生成的消息类型实现的接口类型。 此类型是 protoreflect 包中定义的类型的别名:
  1. type ProtoMessage interface{
  2. ProtoReflect() Message
  3. }
  1. 为了避免填充生成消息的命名空间,该接口仅包含一个返回 protoreflect.Message 的方法,该方法提供对消息内容的访问。

(为什么是别名?因为 protoreflect.Message 有对应的方法返回原始 proto.Message,我们需要避免两个包之间的导入循环。)

protoreflect.Message.Range 方法为消息中的每个填充字段调用一个函数。

  1. m := pb.ProtoReflect()
  2. m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
  3. // ...
  4. return true
  5. })

使用描述字段的协议缓冲区类型的 protoreflect.FieldDescriptor 和包含字段值的 protoreflect.Value 调用范围函数。

protoreflect.FieldDescriptor.Options 方法将字段选项作为 google.protobuf.FieldOptions 消息返回。

  1. opts := fd.Options().(*descriptorpb.FieldOptions)

(为什么是类型断言?由于生成的 descriptorpb 包依赖于 protoreflect,protoreflect 包无法在不导致导入循环的情况下返回具体的选项类型。)然后我们可以检查选项以查看扩展布尔值的值:

  1. if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
  2. return true // don't redact non-sensitive fields
  3. }

请注意,我们在这里查看的是字段描述符,而不是字段值。 我们感兴趣的信息在于协议缓冲区类型系统,而不是 Go 系统。

这也是我们简化 proto 包 API 的一个区域的示例。 原始的 proto.GetExtension 返回一个值和一个错误。 新的 proto.GetExtension 仅返回一个值,如果该字段不存在,则返回该字段的默认值。 在解组时报告扩展解码错误。

一旦我们确定了需要编辑的字段,清除它就很简单:

  1. m.Clear(fd)
  1. 综上所述,我们完整的编辑功能是:
  1. // Redact clears every sensitive field in pb.
  2. func Redact(pb proto.Message) {
  3. m := pb.ProtoReflect()
  4. m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
  5. opts := fd.Options().(*descriptorpb.FieldOptions)
  6. if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
  7. return true
  8. }
  9. m.Clear(fd)
  10. return true
  11. })
  12. }

更完整的实现可能会递归下降到消息值字段。 我们希望这个简单的例子能够让您体验到协议缓冲区反射及其用途。

版本

我们将 Go 协议缓冲区的原始版本称为 APIv1,新版本称为 APIv2。 因为 APIv2 不向后兼容 APIv1,所以我们需要为每个使用不同的模块路径。

(这些 API 版本与 protocol buffer 语言版本不同:proto1、proto2、proto3。APIv1 和 APIv2 是 Go中的具体实现,都支持 proto2 和 proto3 语言版本。)

github.com/golang/ protobuf 模块是 APIv1。

google.golang.org/protobuf 模块是 APIv2。 我们利用了更改导入路径的需要,以切换到与特定托管服务提供商无关的路径。 (我们考虑了 google.golang.org/protobuf/v2,以明确这是 API 的第二个主要版本,但从长远来看,更短的路径是更好的选择。)

我们知道并非所有 用户将以相同的速度迁移到软件包的新主要版本。 有些会很快切换; 其他人可能会无限期地保留在旧版本上。 即使在单个程序中,某些部分也可能使用一种 API,而其他部分使用另一种。 因此,我们必须继续支持使用 APIv1 的程序。

  • github.com/golang/protobuf@v1.3.4 是 APIv1 的最新的 pre-APIv2 版本。
  • github.com/golang/protobuf@v1.4.0 是 APIv1 的一个版本,基于 APIv2 实现。 API 是相同的,但底层实现由新的支持。 此版本包含在 APIv1 和 APIv2 proto.Message 接口之间转换的函数,以简化两者之间的转换。
  • google.golang.org/protobuf@v1.20.0 是 APIv2。 该模块依赖于 github.com/golang/protobuf@v1.4.0,因此任何使用 APIv2 的程序都会自动选择与其集成的 APIv1 版本。

(为什么从 v1.20.0 版本开始?为了清楚起见。我们预计 APIv1 永远不会达到 v1.20.0,因此仅凭版本号就足以明确区分 APIv1 和 APIv2。)

我们打算无限期地保持对 APIv1 的支持。

这种组织确保任何给定的程序将只使用一个协议缓冲区实现,而不管它使用哪个 API 版本。 它允许程序逐渐采用或根本不采用新 API,同时仍能获得新实现的优势。 最小版本选择原则意味着程序可以保留在旧实现上,直到维护者选择更新到新实现(直接或通过更新依赖项)。

附加功能的说明

google.golang.org/protobuf/encoding/protojson 包使用规范的 JSON 映射将协议缓冲区消息与 JSON 相互转换,并修复了旧 jsonpb 包的许多问题,这些问题很难在不给现有用户造成问题的情况下进行更改。

google.golang.org/protobuf/types/dynamicpb 包为协议缓冲区类型在运行时派生的消息提供了 proto.Message 的实现。

google.golang.org/protobuf/testing/protocmp 包提供了将协议缓冲区消息与 github.com/google/cmp 包进行比较的功能。

google.golang.org/protobuf/compiler/protogen 包提供了对编写协议编译器插件的支持。

总结

google.golang.org/protobuf 模块是对 Go 对协议缓冲区支持的重大改革,为反射、自定义消息实现和清理 API 表面提供一流的支持。 我们打算无限期地维护以前的 API 作为新 API 的包装器,允许用户按照自己的步调逐步采用新 API。

我们在此更新中的目标是改进旧 API 的优点,同时解决其缺点。 当我们完成新实现的每个组件时,我们将其用于 Google 的代码库中。 这种增量部署让我们对新 API 的可用性以及新实现的性能和正确性充满信心。 我们相信它已做好生产准备。

我们对这个版本感到兴奋,并希望它将在未来十年及以后为 Go 生态系统服务!