Go 博客
Go 语言的新 Protocol Buffers API
引言
我们很高兴宣布发布 Go API for protocol buffers 的重大修订版,Protocol Buffers 是 Google 的语言中立数据交换格式。
新 API 的动机
Go 的第一个 protocol buffer 绑定由 Rob Pike 于 2010 年 3 月宣布。Go 1 在此之后两年才发布。
自首次发布以来的十年里,该包与 Go 一同发展壮大。其用户的需求也随之增长。
许多人希望编写使用反射来检查 protocol buffer 消息的程序。reflect
包提供了 Go 类型和值的视图,但省略了 protocol buffer 类型系统中的信息。例如,我们可能想编写一个函数来遍历日志条目并清除任何被标记为包含敏感数据的字段。这些标记不是 Go 类型系统的一部分。
另一个普遍的需求是使用 protocol buffer 编译器生成的结构以外的数据结构,例如能够在编译时类型未知的情况下表示消息的动态消息类型。
我们还注意到,一个常见的问题来源是 proto.Message
接口,该接口标识生成的 mesaj 类型的值,但它对这些类型的行为描述非常少。当用户创建实现该接口的类型(通常是在另一个 struct 中嵌入消息时无意间创建的)并将这些类型的值传递给期望生成 mesaj 值 的函数时,程序会崩溃或行为不可预测。
这三个问题都有一个共同的原因和一个共同的解决方案:Message
接口应该完全指定 mesaj 的行为,并且对 Message
值操作的函数应该自由接受任何正确实现该接口的类型。
由于在保持包 API 兼容性的同时无法更改 Message
类型的现有定义,我们决定是时候开始开发一个与现有版本不兼容的新的 major version 的 protobuf 模块了。
今天,我们很高兴发布这个新模块。希望您喜欢它。
反射
反射是新实现的核心特性。类似于 reflect
包提供 Go 类型和值的视图,google.golang.org/protobuf/reflect/protoreflect
包根据 protocol buffer 类型系统提供值的视图。
protoreflect
包的完整描述对于本文来说会太长,但让我们看看如何编写我们之前提到的日志清洗函数。
首先,我们将编写一个 .proto
文件,定义 google.protobuf.FieldOptions
类型的一个扩展,以便我们可以标记字段是否包含敏感信息。
syntax = "proto3";
import "google/protobuf/descriptor.proto";
package golang.example.policy;
extend google.protobuf.FieldOptions {
bool non_sensitive = 50000;
}
我们可以使用这个选项将某些字段标记为非敏感的。
message MyMessage {
string public_name = 1 [(golang.example.policy.non_sensitive) = true];
}
接下来,我们将编写一个 Go 函数,它接受任意的 mesaj 值并移除所有敏感字段。
// Redact clears every sensitive field in pb.
func Redact(pb proto.Message) {
// ...
}
此函数接受一个 proto.Message
,这是一个由所有生成的 mesaj 类型实现的接口类型。此类型是 protoreflect
包中定义的类型的别名。
type ProtoMessage interface{
ProtoReflect() Message
}
为了避免占用生成的 mesaj 的命名空间,该接口只包含一个返回 protoreflect.Message
的方法,该方法提供对 mesaj 内容的访问。
(为什么是别名?因为 protoreflect.Message
有一个对应的方法返回原始的 proto.Message
,并且我们需要避免两个包之间的导入循环。)
protoreflect.Message.Range
方法为 mesaj 中的每个已填充字段调用一个函数。
m := pb.ProtoReflect()
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
// ...
return true
})
范围函数会随同描述字段 protocol buffer 类型的一个 protoreflect.FieldDescriptor
以及包含字段值的一个 protoreflect.Value
一起被调用。
protoreflect.FieldDescriptor.Options
方法将字段选项作为 google.protobuf.FieldOptions
mesaj 返回。
opts := fd.Options().(*descriptorpb.FieldOptions)
(为什么进行类型断言?因为生成的 descriptorpb
包依赖于 protoreflect
,所以 protoreflect
包无法在不引起导入循环的情况下返回具体的 options 类型。)
然后我们可以检查 options 来查看我们的扩展 boolean 的值
if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
return true // don't redact non-sensitive fields
}
请注意,这里我们查看的是字段 descriptor,而不是字段 value。我们感兴趣的信息位于 protocol buffer 类型系统中,而非 Go 类型系统中。
这也是我们简化 proto
包 API 的一个例子。原始的 proto.GetExtension
返回值和错误。新的 proto.GetExtension
只返回值,如果字段不存在则返回其默认值。扩展解码错误会在 Unmarshal
时报告。
一旦我们识别出需要编辑的字段,清除它就很简单了
m.Clear(fd)
将以上所有内容放在一起,我们完整的编辑函数是
// Redact clears every sensitive field in pb.
func Redact(pb proto.Message) {
m := pb.ProtoReflect()
m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
opts := fd.Options().(*descriptorpb.FieldOptions)
if proto.GetExtension(opts, policypb.E_NonSensitive).(bool) {
return true
}
m.Clear(fd)
return true
})
}
更完整的实现可能会递归地深入到 mesaj 类型 的字段中。我们希望这个简单的例子能让您初步了解 protocol buffer 反射及其用途。
版本
我们将 Go protocol buffers 的原始版本称为 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。我们借此机会改变了 import path,以便切换到一个不绑定特定托管提供商的路径。(我们曾考虑使用 google.golang.org/protobuf/v2
来明确这是 API 的第二个 major version,但最终选择了更短的路径,认为这对长期发展更有利。)
我们知道并非所有用户都会以相同的速度迁移到包的新 major version。有些用户会快速切换;另一些可能会无限期地停留在旧版本上。即使在同一个程序中,某些部分可能使用一个 API,而其他部分使用另一个。因此,我们必须继续支持使用 APIv1 的程序。
-
github.com/golang/protobuf@v1.3.4
是 APIv1 在 APIv2 发布之前的最新版本。 -
github.com/golang/protobuf@v1.4.0
是基于 APIv2 实现的 APIv1 版本。API 保持不变,但底层实现已替换为新的。此版本包含用于在 APIv1 和 APIv2proto.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 版本,都只会使用单个 protocol buffer 实现。它允许程序逐步采用新 API,或者完全不采用,同时仍然获得新实现的优势。最小版本选择原则意味着程序可以停留在旧实现上,直到维护者选择更新到新实现(直接更新或通过更新依赖项)。
其他值得注意的特性
google.golang.org/protobuf/encoding/protojson
包使用 规范 JSON 映射 将 protocol buffer 消息转换为 JSON 并从 JSON 转换回来,并解决了旧的 jsonpb
包中的许多难以更改且不会给现有用户带来问题的问题。
google.golang.org/protobuf/types/dynamicpb
包为在运行时推导出其 protocol buffer 类型的 mesaj 提供 proto.Message
实现。
google.golang.org/protobuf/testing/protocmp
包提供了使用 github.com/google/cmp
包比较 protocol buffer 消息的函数。
google.golang.org/protobuf/compiler/protogen
包提供了编写 protocol 编译器插件的支持。
结论
google.golang.org/protobuf
模块是 Go 对 protocol buffers 支持的一次重大改进,为反射、自定义 mesaj 实现以及更清晰的 API 接口提供了 一流的支持。我们打算无限期地维护旧 API 作为新 API 的包装层,允许用户按照自己的节奏逐步采用新 API。
此次更新的目标是在提升旧 API 优势的同时解决其不足。当我们完成新实现的每个组件时,我们都会在 Google 的代码库中投入使用。这种增量式推广使我们对新 API 的可用性以及新实现的性能和正确性充满信心。我们相信它已可用于生产环境。
我们对这次发布感到兴奋,并希望它能在未来十年乃至更长时间里服务于 Go 生态系统!
下一篇文章:Go、Go 社区与疫情
上一篇文章:Go 1.14 已发布
博客索引