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 可以互换使用。

什么是 proto1proto2proto3

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

  • proto3 是语言的当前版本。这是最常用的语言版本。我们鼓励新代码使用 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 protobufs 版本,这将在初始化时导致 panic 或静默地丢弃冲突,并在以后的运行时导致潜在的错误。

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

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

命名空间冲突的常见方式

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

    • 如果 .proto 文件由外部方拥有并且缺少 go_package 选项,则应与该 .proto 文件的所有者协调以指定一个集中式 Go 包,以便大多数用户都可以依赖它。
  • **注意:**追溯更改 .proto 文件的包名称对于用作扩展字段的类型、存储在 google.protobuf.Any 中的类型或 gRPC 服务定义不向后兼容。

    • **警告:**追溯更改 .proto 文件的包名称对于用作扩展字段的类型、存储在 google.protobuf.Any 中的类型或 gRPC 服务定义不向后兼容。

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

为什么 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 定律

什么是 Hyrum 定律,为什么它在这个常见问题解答中?

Hyrum 定律 指出

如果 API 的用户数量足够多,那么您在合同中承诺什么并不重要:您的系统的所有可观察行为都将被某些人依赖。

最新版 Go 协议缓冲区 API 的设计目标是在可能的情况下避免提供我们无法保证将来保持稳定的可观察行为。我们的理念是,在我们不承诺的领域中故意引入不稳定性,要好于给人一种稳定的错觉,而这种稳定性在未来可能会发生变化,届时项目可能已经长期依赖于这种错误的假设。

为什么错误的文本不断变化?

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

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

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

我们不对 Go 实现的 协议缓冲区的 JSON 格式 的长期稳定性做出任何承诺。规范只规定了哪些是有效的 JSON,但没有规定用于指定编组器如何精确格式化给定消息的规范格式。为了避免给人一种输出稳定的错觉,我们故意引入了细微的差异,以便字节对字节的比较很可能失败。

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

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

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

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

其他

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

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

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

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

Go 实现的协议缓冲区力求与其他语言实现保持一致。因此,我们倾向于避免过于专门针对 Go 的特性。特定于 Go 的特性会阻碍协议缓冲区成为语言中立的数据交换格式的目标。

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

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

我可以在 MarshalUnmarshal 中添加选项来自定义它吗?

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

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

我能否自定义 protoc-gen-go 生成的代码?

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