Go 常见问题解答

关于在 Go 中实现 protocol buffer 的常见问题列表,并为每个问题提供了答案。

版本

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

github.com/golang/protobuf 模块是最初的 Go protocol buffer 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 可以互换使用。

proto1proto2proto3 和 editions 是什么?

这些是 protocol buffer *语言* 的修订版。它与 protobuf 的 Go *实现* 不同。

  • Editions 是编写 Protocol Buffers 的最新且推荐的方式。新功能将作为新版本的一部分发布。有关更多信息,请参阅 Protocol Buffer Editions

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

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

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

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

常见问题

go install”: working directory is not part of a module (工作目录不属于模块)

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

在 Go 1.16 及更高版本中,可以通过指定显式版本在模块外部调用 go installgo install google.golang.org/protobuf/cmd/protoc-gen-go@latest

constant -1 overflows protoimpl.EnforceVersion (常量 -1 溢出 protoimpl.EnforceVersion)

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

使用以下命令更新到较新版本

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

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

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

使用以下命令更新到较新版本

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

什么是 protocol buffer 命名空间冲突?

链接到 Go 二进制文件中的所有 protocol buffers 声明都会被插入到一个全局注册表中。

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

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

如何修复 protocol buffer 命名空间冲突?

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

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

  • 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

如何使用 protocol buffer editions?

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

edition = "2023";

然后,protocol buffer 编译器将生成与指定 edition 兼容的 Go 代码。使用 editions,您还可以为您的 .proto 文件启用或禁用特定功能。有关更多信息,请参阅 Protocol Buffer Editions

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

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

edition = "2023";

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

api_level 设置为 API_OPAQUE 时,protocol buffer 编译器生成的 Go 代码会隐藏结构体字段,使其无法再被直接访问。取而代之的是,会创建新的访问器方法来获取、设置或清除字段。

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

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

生成的 protocol buffer 消息类型包含内部状态,即使在等效消息之间也可能不同。

此外,reflect.DeepEqual 函数不知道 protocol buffer 消息的语义,并且可能在不存在差异的地方报告差异。例如,包含 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 protocol buffer API 的一个设计目标是,在可能的情况下,避免提供我们无法承诺在未来保持稳定的可观察行为。我们的理念是,在我们不作任何承诺的领域故意制造不稳定性,比给人一种稳定性的假象,然后这种假象在未来某个项目可能已经长期依赖这种错误假设之后发生改变要好。

为什么错误的文本内容一直在变?

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

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

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

我们对 Go 实现的 protocol buffers 的 JSON 格式 的长期稳定性不作任何承诺。该规范仅规定了什么是有效的 JSON,但没有为编组器应如何*确切地*格式化给定消息提供*规范*格式的规范。为了避免给人一种输出是稳定的假象,我们故意引入微小的差异,以便逐字节比较很可能会失败。

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

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

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

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

其他

如何将 protocol buffer 消息用作哈希键?

您需要规范序列化,即 protocol buffer 消息的编组输出保证随时间推移是稳定的。不幸的是,目前不存在规范序列化的规范。您需要自己编写或找到避免需要它的方法。

我可以为 Go protocol buffer 实现添加新功能吗?

也许吧。我们总是喜欢建议,但我们对添加新事物非常谨慎。

Go 实现的 protocol buffers 努力与其他语言的实现保持一致。因此,我们倾向于避开过于专门针对 Go 的功能。特定于 Go 的功能阻碍了 protocol buffers 成为一种与语言无关的数据交换格式的目标。

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

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

我可以为 MarshalUnmarshal 添加一个选项来自定义它吗?

仅当该选项在其他实现(例如 C++、Java)中存在时才可以。protocol buffers 的编码(二进制、JSON 和文本)必须在各个实现中保持一致,因此用一种语言编写的程序能够读取由另一种语言编写的消息。

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

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

总的来说,不可以。Protocol buffers 旨在成为一种与语言无关的数据交换格式,而特定于实现的自定义与这一意图背道而驰。