Go 常见问题解答

关于在 Go 中实现协议缓冲区的常见问题列表,并附有每个问题的答案。

版本

github.com/golang/protobufgoogle.golang.org/protobuf 有什么区别?

github.com/golang/protobuf 模块是原始的 Go 协议缓冲区 API。

google.golang.org/protobuf 模块是此 API 的更新版本,旨在实现简单性、易用性和安全性。更新后的 API 的主要功能是支持反射以及用户面向的 API 与底层实现的解耦。

我们建议您在新代码中使用 google.golang.org/protobuf

github.com/golang/protobufv1.4.0 及更高版本包装了新实现,并允许程序逐步采用新 API。例如,github.com/golang/protobuf/ptypes 中定义的知名类型只是新模块中定义的那些类型的别名。因此,google.golang.org/protobuf/types/known/emptypbgithub.com/golang/protobuf/ptypes/empty 可以互换使用。

什么是 proto1, proto2, proto3 和版本(editions)?

这些是协议缓冲区 语言 的修订版。它与 protobufs 的 Go 实现 不同。

  • 版本(Editions)是编写 Protocol Buffers 的最新和推荐方式。新功能将作为新版本的一部分发布。欲了解更多信息,请参阅协议缓冲区版本

  • proto3 是该语言的旧版本。我们鼓励新代码使用版本。

  • proto2 是该语言的旧版本。尽管已被 proto3 和版本取代,proto2 仍然得到完全支持。

  • proto1 是该语言的过时版本。它从未作为开源发布。

有几种不同的 Message 类型。我应该使用哪种?

常见问题

go install”: 工作目录不是模块的一部分

在 Go 1.15 及以下版本中,您已设置环境变量 GO111MODULE=on 并在模块目录之外运行 go install 命令。请将 GO111MODULE=auto 或取消设置该环境变量。

在 Go 1.16 及以上版本中,通过指定明确的版本可以在模块之外调用 go installgo install google.golang.org/protobuf/cmd/protoc-gen-go@latest

常量 -1 溢出 protoimpl.EnforceVersion

您正在使用生成的 .pb.go 文件,该文件需要更新版本的 "google.golang.org/protobuf" 模块。

更新到新版本,使用

go get -u google.golang.org/protobuf/proto

未定义: "github.com/golang/protobuf/proto".ProtoPackageIsVersion4

您正在使用生成的 .pb.go 文件,该文件需要更新版本的 "github.com/golang/protobuf" 模块。

更新到新版本,使用

go get -u github.com/golang/protobuf/proto

什么是协议缓冲区命名空间冲突?

所有链接到 Go 二进制文件中的协议缓冲区声明都插入到全局注册表中。

每个 protobuf 声明(例如,枚举、枚举值或消息)都有一个绝对名称,它是包名.proto 源文件中声明的相对名称的连接(例如,my.proto.package.MyMessage.NestedMessage)。protobuf 语言假定所有声明都是全局唯一的。

如果链接到 Go 二进制文件中的两个 protobuf 声明具有相同的名称,则会导致命名空间冲突,并且注册表无法通过名称正确解析该声明。根据所使用的 Go protobuf 版本,这将在初始化时引发恐慌(panic)或静默丢弃冲突,并可能在运行时导致潜在的错误。

如何修复协议缓冲区命名空间冲突?

解决命名空间冲突的最佳方法取决于冲突发生的原因。

命名空间冲突的常见发生方式

  • Vendored .proto 文件。 当单个 .proto 文件被生成到两个或多个 Go 包中并链接到同一个 Go 二进制文件时,它会在生成的 Go 包中的每个 protobuf 声明上发生冲突。这通常发生在 .proto 文件被 vendored 并从中生成 Go 包时,或者生成的 Go 包本身被 vendored 时。用户应该避免 vendoring,而是依赖于该 .proto 文件的集中式 Go 包。

    • 如果一个 .proto 文件由外部方拥有,并且缺少 go_package 选项,那么您应该与该 .proto 文件的所有者协调,以指定一个集中式的 Go 包,供大多数用户依赖。
  • 缺少或通用的 proto 包名。如果一个 .proto 文件没有指定包名或使用了过于通用的包名(例如,“my_service”),那么该文件中声明与宇宙中其他地方的声明发生冲突的可能性很高。我们建议每个 .proto 文件都有一个经过精心选择的全局唯一的包名(例如,以公司名称作为前缀)。

google.golang.org/protobuf 模块的 v1.26.0 版本开始,当 Go 程序启动时,如果链接了多个冲突的 protobuf 名称,将报告硬错误。虽然最好是修复冲突的来源,但可以通过两种方式立即解决致命错误:

  1. 在编译时。 处理冲突的默认行为可以通过链接器初始化的变量在编译时指定:go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn"

  2. 在程序执行时。 执行特定 Go 二进制文件时处理冲突的行为可以通过环境变量设置:GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn ./main

如何使用协议缓冲区版本(editions)?

要使用 protobuf 版本,您必须在 .proto 文件中指定该版本。例如,要使用 2023 版本,请在 .proto 文件的顶部添加以下内容:

edition = "2023";

协议缓冲区编译器将生成与指定版本兼容的 Go 代码。通过版本,您还可以为 .proto 文件启用或禁用特定功能。有关更多信息,请参阅 协议缓冲区版本

如何控制生成的 Go 代码的行为?

通过版本,您可以通过在 .proto 文件中启用或禁用特定功能来控制生成的 Go 代码的行为。例如,要设置实现的 API 行为,您可以在 .proto 文件中添加以下内容:

edition = "2023";

option features.(pb.go).api_level = API_OPAQUE;

api_level 设置为 API_OPAQUE 时,协议缓冲区编译器生成的 Go 代码会隐藏结构字段,使其无法再直接访问。相反,会创建新的访问器方法来获取、设置或清除字段。

有关可用功能及其描述的完整列表,请参阅版本功能

为什么 reflect.DeepEqual 对 protobuf 消息的行为出乎意料?

生成的协议缓冲区消息类型包含内部状态,即使在等效消息之间也可能有所不同。

此外,reflect.DeepEqual 函数不了解协议缓冲区消息的语义,并且可能会报告不存在的差异。例如,包含 nil 映射的映射字段和包含零长度、非 nil 映射的映射字段在语义上是等效的,但会被 reflect.DeepEqual 报告为不相等。

使用 proto.Equal 函数比较消息值。

在测试中,您还可以使用 "github.com/google/go-cmp/cmp" 包和 protocmp.Transform() 选项。cmp 包可以比较任意数据结构,而 cmp.Diff 会生成人类可读的值差异报告。

if diff := cmp.Diff(a, b, protocmp.Transform()); diff != "" {
  t.Errorf("unexpected difference:\n%v", diff)
}

海勒姆定律(Hyrum’s Law)

什么是海勒姆定律,为什么它出现在这个常见问题解答中?

海勒姆定律指出

当一个 API 的用户数量足够多时,你承诺的契约内容就不重要了:你系统的所有可观察行为都将被某些人所依赖。

最新版 Go 协议缓冲区 API 的一个设计目标是,在可能的情况下,避免提供我们无法保证将来保持稳定的可观察行为。我们的理念是,在不作任何承诺的领域,蓄意的不稳定性优于制造稳定性的假象,因为这种假象可能会在某个项目长期依赖这种错误假设后在将来发生改变。

为什么错误文本一直在变化?

依赖于精确错误文本的测试是脆弱的,并且在文本更改时经常中断。为了阻止在测试中不安全地使用错误文本,此模块生成的错误文本是故意不稳定的。

如果您需要识别错误是否由 protobuf 模块生成,我们保证所有错误都将根据 errors.Is 匹配 proto.Error

为什么 protojson 的输出一直在变化?

我们不保证 Go 对协议缓冲区的 JSON 格式实现的长期稳定性。该规范只规定了什么是有效的 JSON,但没有规定 marshaler 应该如何精确地格式化给定消息的规范格式。为了避免给人一种输出是稳定的假象,我们故意引入微小的差异,以便逐字节比较很可能失败。

为了获得一定程度的输出稳定性,我们建议通过 JSON 格式化程序运行输出。

为什么 prototext 的输出一直在变化?

我们不承诺 Go 文本格式实现的长期稳定性。协议缓冲区文本格式没有规范规范,我们希望保留将来改进 prototext 包输出的能力。由于我们不承诺包输出的稳定性,我们故意引入了不稳定性,以阻止用户依赖它。

为了获得一定程度的稳定性,我们建议通过 txtpbfmt 程序传递 prototext 的输出。格式化程序可以直接在 Go 中使用 parser.Format 调用。

杂项

如何将协议缓冲区消息用作哈希键?

您需要规范化序列化,即协议缓冲区消息的编组输出保证随时间保持稳定。不幸的是,目前没有规范化序列化的规范。您需要自己编写一个,或者找到一种避免需要它的方法。

我可以为 Go 协议缓冲区实现添加新功能吗?

也许吧。我们总是乐于接受建议,但我们对添加新事物非常谨慎。

Go 协议缓冲区的实现力求与其他语言实现保持一致。因此,我们倾向于避免过于专注于 Go 的功能。Go 特定功能阻碍了协议缓冲区成为一种与语言无关的数据交换格式的目标。

除非您的想法特定于 Go 实现,否则您应该加入 protobuf 讨论组并在那里提出。

如果您对 Go 实现有想法,请在我们的问题跟踪器上提交问题:https://github.com/golang/protobuf/issues

我可以为 MarshalUnmarshal 添加选项以进行自定义吗?

仅当该选项存在于其他实现(例如,C++、Java)中时。协议缓冲区(二进制、JSON 和文本)的编码必须在不同实现之间保持一致,因此用一种语言编写的程序能够读取另一种语言编写的消息。

我们不会向 Go 实现添加任何影响 Marshal 函数输出数据或 Unmarshal 函数读取数据的选项,除非在至少一个其他受支持的实现中存在等效选项。

我可以自定义 protoc-gen-go 生成的代码吗?

一般来说,不能。协议缓冲区旨在成为一种与语言无关的数据交换格式,而特定于实现的自定义功能与该意图背道而驰。